diff --git a/.github/ISSUE_TEMPLATE/02_bug_report.yml b/.github/ISSUE_TEMPLATE/02_bug_report.yml index d8567aca2..113b0dbc9 100644 --- a/.github/ISSUE_TEMPLATE/02_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/02_bug_report.yml @@ -23,6 +23,7 @@ body: - label: lucide-vue - label: lucide-vue-next - label: lucide-astro + - label: '@lucide/icons' - label: Figma plugin - label: source/main - label: other/not relevant diff --git a/.github/ISSUE_TEMPLATE/04_feature_request.yml b/.github/ISSUE_TEMPLATE/04_feature_request.yml index 63a2469ec..1fb43bcfa 100644 --- a/.github/ISSUE_TEMPLATE/04_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/04_feature_request.yml @@ -23,6 +23,7 @@ body: - label: lucide-vue - label: lucide-vue-next - label: lucide-astro + - label: '@lucide/icons' - label: Figma plugin - label: all JS packages - label: site diff --git a/.github/labeler.yml b/.github/labeler.yml index b690d1926..8f87a6e86 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -85,6 +85,12 @@ - any-glob-to-any-file: - 'packages/astro/*' +# For changes in the @lucide/icons package +❇️ lucide-icons: + - changed-files: + - any-glob-to-any-file: + - 'packages/lucide-icons/*' + # For changes in the lucide static package 🪨 static package: - changed-files: diff --git a/.github/workflows/icons.yml b/.github/workflows/icons.yml new file mode 100644 index 000000000..42865b6b8 --- /dev/null +++ b/.github/workflows/icons.yml @@ -0,0 +1,43 @@ +name: Lucide Icons Checks + +on: + pull_request: + paths: + - packages/icons/** + - packages/shared/** + - tools/build-icons/** + - tools/rollup-plugins/** + - pnpm-lock.yaml + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v6 + with: + cache: 'pnpm' + node-version-file: 'package.json' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm --filter @lucide/icons build + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v6 + with: + cache: 'pnpm' + node-version-file: 'package.json' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Test + run: pnpm --filter @lucide/icons test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14e6d7513..e1798f55c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,6 +57,7 @@ jobs: 'lucide-svelte', '@lucide/astro', '@lucide/svelte', + '@lucide/icons', '@lucide/vue', ] steps: diff --git a/docs/.vitepress/data/packageData.json b/docs/.vitepress/data/packageData.json index 9027c2c75..ff3c8bc4d 100644 --- a/docs/.vitepress/data/packageData.json +++ b/docs/.vitepress/data/packageData.json @@ -153,5 +153,23 @@ "href": "https://www.npmjs.com/package/lucide-static" } ] + }, + "@lucide/icons": { + "docsAlias": "icons", + "packageDirname": "icons", + "order": 10, + "icon": "ts", + "shields": [ + { + "alt": "npm", + "src": "https://img.shields.io/npm/v/@lucide/icons", + "href": "https://www.npmjs.com/package/@lucide/icons" + }, + { + "alt": "npm", + "src": "https://img.shields.io/npm/dw/@lucide/icons", + "href": "https://www.npmjs.com/package/@lucide/icons" + } + ] } } diff --git a/docs/.vitepress/sidebar.ts b/docs/.vitepress/sidebar.ts index 0c95d87db..79eeba485 100644 --- a/docs/.vitepress/sidebar.ts +++ b/docs/.vitepress/sidebar.ts @@ -104,6 +104,10 @@ const sidebar: UserConfig['themeConfig']['sidebar'] = { text: 'Lucide Static', link: '/guide/packages/lucide-static', }, + { + text: 'Icon data & helpers', + link: '/guide/packages/icons', + }, ], }, { diff --git a/docs/guide/advanced/aliased-names.md b/docs/guide/advanced/aliased-names.md index 67ace7ec7..32abb7b5e 100644 --- a/docs/guide/advanced/aliased-names.md +++ b/docs/guide/advanced/aliased-names.md @@ -79,6 +79,16 @@ declare module "lucide-react-native" { } ``` +```ts [@lucide-icons] +declare module "@lucide/icons" { + // Prefixed import names + export * from "@lucide/icons/dist/lucide-icons.prefixed"; + // or + // Suffixed import names + export * from "@lucide/icons/dist/lucide-icons.suffixed"; +} +``` + ::: Place this in your project root or in a folder where your tsconfig.json is located, or locate it in your defined type directory. diff --git a/docs/guide/packages/icons.md b/docs/guide/packages/icons.md new file mode 100644 index 000000000..3be3f9c16 --- /dev/null +++ b/docs/guide/packages/icons.md @@ -0,0 +1,172 @@ +# @lucide/icons + +`@lucide/icons` is a helper library that exports Lucide **icon data** in a tree-shakable format, also providing utilities for dynamic importing icons. + +It intentionally ships **no real rendering logic or components** — other packages (for example [`@lucide/angular`](http://npmjs.com/package/@lucide/angular)) can consume this data to render icons in their respective + frameworks. You can also use this package to build third-party integrations for frameworks we don't (yet) support. + +## Installation + +::: code-group + +```sh [pnpm] +pnpm install @lucide/icons +``` + +```sh [yarn] +yarn add @lucide/icons +``` + +```sh [npm] +npm install @lucide/icons +``` + +```sh [bun] +bun add @lucide/icons +``` + +::: + +## Icon data format + +Each icon is described by the following interface: + +```typescript +export type LucideIconData = { + name: string; + node: LucideIconNode[]; +} & ( + | { size: number } + | { width: number; height: number; } +); +``` + +| name | type | description | +|------------------------------|--------------------|--------------------------------------------------------------------| +| `name` | `string` | The name of the icon. | +| `node` | `LucideIconNode[]` | SVG child nodes as `[tagName, attributes]` tuples. | +| `size` or `width` & `height` | `number` | The dimensions of the icon (`size` is shorthand for square icons). | + +## How to use + +Icons can be imported individually. Only the icons you import end up referenced by your application code — the rest will be eliminated by tree-shaking. + +```ts +import { House } from '@lucide/icons'; +// House is icon data (not a rendered component). +``` + +## Building icons + +`@lucide/icons` ships small helpers that convert Lucide icon data into different render-ready outputs. +All builders accept the same `params` object (`LucideBuildParams`) to customize the generated SVG. + +### Build parameters + +The following parameters are supported (names reflect the current implementation): + +| param | type | effect | +|-----------------------|--------------------------|------------------------------------------------------------------------------------| +| `color` | `string` | Sets `stroke` (defaults to `currentColor`). | +| `size` | `number` | Sets both `width` and `height` (defaults to 24). | +| `width` | `number` | Sets `width` only. | +| `height` | `number` | Sets `height` only. | +| `strokeWidth` | `number` | Sets `stroke-width` (defaults to 2). | +| `absoluteStrokeWidth` | `boolean` | Adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements. | +| `className` | `string` | Appended to the generated `class` attribute. | +| `attributes` | `Record` | Add or override any generated SVG attributes (including `class`, `viewBox`, etc.). | + +::: info +SVG attributes generated by the builders include a default Lucide setup (`xmlns`, `viewBox`, `fill="none"`, `stroke="currentColor"`, `stroke-width="2"`, `stroke-linecap="round"`, `stroke-linejoin="round"`), plus a class string of the form: `lucide lucide-{iconName}`. +::: + +### `buildLucideIconNode` + +Creates a root SVG node in an svgson-like format: + +```ts +import { buildLucideIconNode } from '@lucide/icons/builders'; +import { House } from '@lucide/icons'; + +const node = buildLucideIconNode(House, { + size: 32, + strokeWidth: 1.5, + className: 'my-icon', +}); + +// -> ['svg', attributes, children] +``` + +This is useful if you want to plug Lucide icons into your own renderer, templating system, or framework integration. + +### `buildLucideSvg` + +Creates an SVG string: + +```ts +import { buildLucideSvg } from '@lucide/icons/builders'; +import { House } from '@lucide/icons'; + +const svg = buildLucideSvg(House, { size: 24, color: '#111' }); +``` + +### `buildLucideIconElement` + +Creates an actual DOM element (SVG) within the provided document: + +```ts +import { buildLucideIconElement } from '@lucide/icons/builders'; +import { House } from '@lucide/icons'; + +const el = buildLucideIconElement(document, House, { size: 24 }); +document.body.appendChild(el); +``` + +### `buildLucideDataUri` + +Creates a base64-encoded SVG data URI from a Lucide icon object. + +This helper works in both browsers and Node.js: +- In browsers it uses `btoa` (with proper UTF-8 handling) +- In Node.js it falls back to `Buffer` + +```ts +import { buildLucideDataUri } from '@lucide/icons/builders'; +import { House } from '@lucide/icons'; + +const uri = buildLucideDataUri(House, { size: 24 }); +``` + +The returned value can be used directly in places such as: +- `` +- CSS `background-image` +- Canvas drawing +- Inline data URLs in HTML or SVG + +::: tip Environment notes +- The SVG is encoded as UTF-8 before base64 conversion to ensure correct handling of non-ASCII characters. +- No runtime configuration is required — the function automatically selects the appropriate encoding strategy. +- If neither `btoa` nor `Buffer` is available, an error is thrown. +::: + +## Dynamic imports + +Dynamic imports are useful when you only know the icon name at runtime (for example, icon names stored in a database or a CMS). For purely static use cases, prefer direct imports for the best tree-shaking results. + +::: tip +Validate `iconName` before indexing the map (and provide a fallback icon) to avoid runtime errors. +::: +## Dynamic imports + +Dynamic imports are useful when the icon name is only known at runtime (for example, icon names stored in a CMS or database). For purely static usage, prefer direct imports for maximum tree-shaking. + +```ts +import { lucideDynamicIconImports } from '@lucide/icons/dynamic'; + +const name = 'house'; +const icon = await lucideDynamicIconImports[name]?.(); + +if (!icon) { + // handle unknown icon name (fallback) +} +``` diff --git a/docs/public/framework-logos/ts.svg b/docs/public/framework-logos/ts.svg new file mode 100644 index 000000000..cb42df855 --- /dev/null +++ b/docs/public/framework-logos/ts.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/public/package-logos/icons.svg b/docs/public/package-logos/icons.svg new file mode 100644 index 000000000..30396d6e0 --- /dev/null +++ b/docs/public/package-logos/icons.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/icons/.npmignore b/packages/icons/.npmignore new file mode 100644 index 000000000..fc28543eb --- /dev/null +++ b/packages/icons/.npmignore @@ -0,0 +1,10 @@ +stats +node_modules +tests +scripts +build +src +babel.config.js +jest.config.js +rollup.config.js +yarn.error.log diff --git a/packages/icons/README.md b/packages/icons/README.md new file mode 100644 index 000000000..971e2cb34 --- /dev/null +++ b/packages/icons/README.md @@ -0,0 +1,82 @@ +

+ + + +

+ +

+Lucide helper library that exports icon data. +

+ +
+ [![npm](https://img.shields.io/npm/v/@lucide/icons?color=blue)](https://www.npmjs.com/package/@lucide/icons) + ![NPM Downloads](https://img.shields.io/npm/dw/@lucide/icons) + [![GitHub](https://img.shields.io/github/license/lucide-icons/lucide)](https://lucide.dev/license) +
+ +

+ About + · + Icons + · + Documentation + · + License +

+ +# @lucide/icons + +A helper library that exports Lucide icon data in a tree-shakable format, also providing utilities for dynamic importing icons. + +## Installation + +```sh +pnpm add @lucide/icons +``` + +```sh +npm install @lucide/icons +``` + +```sh +yarn add @lucide/icons +``` + +```sh +bun add @lucide/icons +``` + +### CDN + +```html + + + + + +``` + +## Documentation + +For full documentation, visit [lucide.dev](https://lucide.dev/guide/packages/icons) + +## Community + +Join the [Discord server](https://discord.gg/EH6nSts) to chat with the maintainers and other users. + +## License + +Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/license). + +## Sponsors + + + Powered by Vercel + + +DigitalOcean Referral Badge + +### Awesome backers 🍺 + +Scipress sponsor badge +pdfme sponsor badge diff --git a/packages/icons/package.json b/packages/icons/package.json new file mode 100644 index 000000000..165cbd181 --- /dev/null +++ b/packages/icons/package.json @@ -0,0 +1,77 @@ +{ + "name": "@lucide/icons", + "description": "A Lucide icon library that contains icon data in a standard Lucide format.", + "version": "0.0.1", + "license": "ISC", + "homepage": "https://lucide.dev", + "bugs": "https://github.com/lucide-icons/lucide/issues", + "repository": { + "type": "git", + "url": "https://github.com/lucide-icons/lucide.git", + "directory": "packages/lucide" + }, + "keywords": [ + "Lucide", + "HTML", + "Feather", + "Icons", + "Icon", + "SVG", + "Feather Icons" + ], + "amdName": "lucide-icons", + "source": "src/lucide-icons.ts", + "main": "dist/cjs/lucide-icons.cjs", + "module": "dist/esm/lucide-icons.mjs", + "types": "dist/esm/lucide-icons.d.ts", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/esm/lucide-icons.d.ts", + "import": "./dist/esm/lucide-icons.mjs", + "require": "./dist/cjs/lucide-icons.cjs", + "default": "./dist/esm/lucide-icons.mjs" + }, + "./icons/*": { + "import": "./dist/esm/icons/*.mjs", + "default": "./dist/esm/icons/*.mjs" + }, + "./dynamic": { + "types": "./dist/esm/dynamic.d.ts", + "import": "./dist/esm/dynamic.mjs", + "require": "./dist/cjs/dynamic.cjs", + "default": "./dist/esm/dynamic.mjs" + }, + "./build": { + "types": "./dist/esm/build.d.ts", + "import": "./dist/esm/build.mjs", + "require": "./dist/cjs/build.cjs", + "default": "./dist/esm/build.mjs" + } + }, + "scripts": { + "build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm build:bundle", + "copy:license": "cp ../../LICENSE ./LICENSE", + "clean": "rm -rf dist && rm -rf stats && rm -rf ./src/icons/*.ts", + "build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mjs --renderUniqueKey --withAliases --withDynamicImports --separateAliasesFile --aliasesFileExtension=.ts --iconFileExtension=.ts --exportFileName=index.ts", + "build:bundle": "rollup -c rollup.config.mjs", + "test": "pnpm build:icons && vitest run", + "test:watch": "vitest watch", + "test:update": "vitest -u", + "version": "pnpm version --git-tag-version=false" + }, + "devDependencies": { + "@lucide/build-icons": "workspace:*", + "@lucide/helpers": "^1.0.0", + "@lucide/rollup-plugins": "workspace:*", + "@rollup/plugin-replace": "^6.0.2", + "@testing-library/jest-dom": "^6.6.3", + "jest-serializer-html": "^7.1.0", + "rollup": "^4.53.3", + "rollup-plugin-dts": "^6.2.3", + "rollup-plugin-preserve-directives": "^0.4.0", + "typescript": "^5.8.3", + "vite": "^7.2.4", + "vitest": "^4.0.12" + } +} diff --git a/packages/icons/rollup.config.mjs b/packages/icons/rollup.config.mjs new file mode 100644 index 000000000..232afd24f --- /dev/null +++ b/packages/icons/rollup.config.mjs @@ -0,0 +1,87 @@ +import plugins from '@lucide/rollup-plugins'; +import pkg from './package.json' with { type: 'json' }; +import dts from 'rollup-plugin-dts'; + +const packageName = '@lucide/icons'; +const outputFileName = 'lucide-icons'; +const inputs = [`src/lucide-icons.ts`]; +const bundles = [ + { + format: 'cjs', + inputs, + extension: 'cjs', + }, + { + format: 'esm', + inputs: [...inputs, './src/dynamic.ts', './src/build.ts'], + preserveModules: true, + extension: 'mjs', + }, +]; + +const configs = bundles + .map( + ({ + inputs, + outputDir = 'dist', + outputFile, + format, + minify, + preserveModules, + entryFileNames, + external = [], + paths, + extension = 'js', + }) => + inputs.map((input) => ({ + input, + plugins: [...plugins({ pkg, minify })], + external, + output: { + name: packageName, + entryFileNames, + ...(preserveModules + ? { + dir: `${outputDir}/${format}`, + entryFileNames: `[name].${extension}`, + } + : { + file: + outputFile ?? + `${outputDir}/${format}/${outputFileName}${minify ? '.min' : ''}.${extension}`, + }), + paths, + format, + sourcemap: true, + preserveModules, + exports: 'named', + globals: {}, + preserveModulesRoot: 'src', + }, + })), + ) + .flat(); + +export default [ + ...[ + outputFileName, + `${outputFileName}.prefixed`, + `${outputFileName}.suffixed`, + 'dynamic', + 'build', + ].map((filename) => ({ + input: `./src/${filename}.ts`, + output: [ + { + file: `dist/esm/${filename}.d.ts`, + format: 'esm', + }, + { + file: `dist/cjs/${filename}.d.cts`, + format: 'cjs', + }, + ], + plugins: [dts()], + })), + ...configs, +]; diff --git a/packages/icons/scripts/exportTemplate.mjs b/packages/icons/scripts/exportTemplate.mjs new file mode 100644 index 000000000..5c5280496 --- /dev/null +++ b/packages/icons/scripts/exportTemplate.mjs @@ -0,0 +1,32 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import base64SVG from '@lucide/build-icons/utils/base64SVG'; + +export default async ({ + componentName, + iconName, + getSvg, + deprecated, + deprecationReason, + iconData, +}) => { + const svgContents = await getSvg(); + const svgBase64 = base64SVG(svgContents); + + return ` +import type { LucideIconData } from '../types'; + +/** + * @name ${iconName} + * @description Lucide SVG icon node. + * + * @preview ![img](data:image/svg+xml;base64,${svgBase64}) - https://lucide.dev/icons/${iconName} + * @see https://lucide.dev/guide/packages/lucide - Documentation + * + * @returns {Array} + * ${deprecated ? `@deprecated ${deprecationReason}` : ''} + */ +const ${componentName}: LucideIconData = ${JSON.stringify(iconData)} + +export default ${componentName}; +`; +}; diff --git a/packages/icons/src/aliases/index.ts b/packages/icons/src/aliases/index.ts new file mode 100644 index 000000000..bbe66d0a2 --- /dev/null +++ b/packages/icons/src/aliases/index.ts @@ -0,0 +1,3 @@ +export * from './aliases'; +export * from './prefixed'; +export * from './suffixed'; diff --git a/packages/icons/src/build.ts b/packages/icons/src/build.ts new file mode 100644 index 000000000..7ede1c55f --- /dev/null +++ b/packages/icons/src/build.ts @@ -0,0 +1,4 @@ +export { default as buildLucideIconNode } from './buildLucideIconNode'; +export { default as buildLucideDataUri } from './buildLucideDataUri'; +export { default as buildLucideIconElement } from './buildLucideIconElement'; +export { default as buildLucideSvg } from './buildLucideSvg'; diff --git a/packages/icons/src/buildLucideDataUri.ts b/packages/icons/src/buildLucideDataUri.ts new file mode 100644 index 000000000..64aa7526c --- /dev/null +++ b/packages/icons/src/buildLucideDataUri.ts @@ -0,0 +1,30 @@ +import { LucideBuildParams, LucideIconData } from './types'; +import buildLucideSvg from './buildLucideSvg'; + +/** + * Creates a base64 encoded data URI from a Lucide icon object. + * + * @param icon The icon to build. + * @param params Additional build parameters. + */ +function buildLucideDataUri(icon: LucideIconData, params: LucideBuildParams = {}) { + const svg = buildLucideSvg(icon, params); + + // Browser + if (typeof btoa === 'function') { + // Ensure proper UTF-8 handling before base64 + const utf8Bytes = new TextEncoder().encode(svg); + let binary = ''; + for (const element of utf8Bytes) binary += String.fromCodePoint(element); + return `data:image/svg+xml;base64,${btoa(binary)}`; + } + + // Node.js (and other JS runtimes with Buffer) + if (typeof Buffer !== 'undefined') { + return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf8').toString('base64')}`; + } + + throw new Error('No base64 encoder available in this environment.'); +} + +export default buildLucideDataUri; diff --git a/packages/icons/src/buildLucideIconElement.ts b/packages/icons/src/buildLucideIconElement.ts new file mode 100644 index 000000000..a819d0fde --- /dev/null +++ b/packages/icons/src/buildLucideIconElement.ts @@ -0,0 +1,34 @@ +import { LucideBuildParams, LucideIconData, LucideIconNode } from './types'; +import buildLucideIconNode from './buildLucideIconNode'; + +const buildDomElement = ( + document: Document, + [tagName, attributes, children = []]: LucideIconNode, +): Element => { + const element = document.createElementNS('http://www.w3.org/2000/svg', tagName); + for (const [attrName, value] of Object.entries(attributes)) { + element.setAttribute(attrName, value); + } + for (const node of children) { + const childNode = buildDomElement(document, node); + element.appendChild(childNode); + } + return element; +}; + +/** + * Creates an SvgElement from a Lucide icon object. + * + * @param document The document to create the Element in. + * @param icon The icon to build. + * @param params Additional build parameters. + */ +function buildLucideIconElement( + document: Document, + icon: LucideIconData, + params: LucideBuildParams = {}, +) { + return buildDomElement(document, buildLucideIconNode(icon, params)); +} + +export default buildLucideIconElement; diff --git a/packages/icons/src/buildLucideIconNode.ts b/packages/icons/src/buildLucideIconNode.ts new file mode 100644 index 000000000..a918ffbbd --- /dev/null +++ b/packages/icons/src/buildLucideIconNode.ts @@ -0,0 +1,55 @@ +import { LucideBuildParams, LucideIconData, LucideIconNode } from './types'; + +const defaultAttributes = { + xmlns: 'http://www.w3.org/2000/svg', + width: '24', + height: '24', + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '2', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', +}; + +/** + * Creates a Lucide icon node (an svgson-like format) from a Lucide icon object. + * + * @param icon The icon to build. + * @param params Additional build parameters. + */ +function buildLucideIconNode(icon: LucideIconData, params: LucideBuildParams = {}): LucideIconNode { + const viewBoxWidth = ('size' in icon ? icon.size : icon.width) ?? defaultAttributes.width; + const viewBoxHeight = ('size' in icon ? icon.size : icon.height) ?? defaultAttributes.height; + const aliasClassNames = icon.aliases?.map((alias) => `lucide-${alias}`) ?? []; + const classList = ['lucide', `lucide-${icon.name}`, ...aliasClassNames]; + if (params['className']) { + classList.push(params['className']); + } + const attributes = { + ...defaultAttributes, + ...('color' in params && { stroke: params['color'] }), + ...('size' in params && + params['size'] && { + width: params['size'].toString(10), + height: params['size'].toString(10), + }), + ...('width' in params && params['width'] && { width: params['width'].toString(10) }), + ...('height' in params && params['height'] && { height: params['height'].toString(10) }), + ...('strokeWidth' in params && + params['strokeWidth'] && { 'stroke-width': params['strokeWidth'].toString(10) }), + class: classList.join(' '), + viewBox: `0 0 ${viewBoxWidth} ${viewBoxHeight}`, + ...('attributes' in params && params.attributes), + }; + return [ + 'svg', + attributes, + icon.node.map(([name, attrs]) => [ + name, + params['absoluteStrokeWidth'] ? { 'vector-effect': 'non-scaling-stroke', ...attrs } : attrs, + ]), + ]; +} + +export default buildLucideIconNode; diff --git a/packages/icons/src/buildLucideSvg.ts b/packages/icons/src/buildLucideSvg.ts new file mode 100644 index 000000000..27bca4a91 --- /dev/null +++ b/packages/icons/src/buildLucideSvg.ts @@ -0,0 +1,19 @@ +import { LucideBuildParams, LucideIconData, LucideIconNode } from './types'; +import buildLucideIconNode from './buildLucideIconNode'; + +const buildDomNode = ([tagName, attributes, children = []]: LucideIconNode): string => + `<${tagName} ${Object.entries(attributes) + .map(([attrName, value]) => `${attrName}="${value}"`) + .join(' ')}>${children?.map((child) => buildDomNode(child)).join('')}`; + +/** + * Creates an SVG string from a Lucide icon object. + * + * @param icon The icon to build. + * @param params Additional build parameters. + */ +function buildLucideSvg(icon: LucideIconData, params: LucideBuildParams = {}) { + return buildDomNode(buildLucideIconNode(icon, params)); +} + +export default buildLucideSvg; diff --git a/packages/icons/src/dynamic.ts b/packages/icons/src/dynamic.ts new file mode 100644 index 000000000..098da1204 --- /dev/null +++ b/packages/icons/src/dynamic.ts @@ -0,0 +1,2 @@ +export { lucideIconNames, type LucideIconName } from './dynamicIcon'; +export { default as lucideDynamicIconImports } from './dynamicIconImports'; diff --git a/packages/icons/src/dynamicIcon.ts b/packages/icons/src/dynamicIcon.ts new file mode 100644 index 000000000..2b1432652 --- /dev/null +++ b/packages/icons/src/dynamicIcon.ts @@ -0,0 +1,8 @@ +import dynamicIconImports from './dynamicIconImports'; + +export type LucideIconName = keyof typeof dynamicIconImports; + +/** + * The list of available Lucide icon names. + */ +export const lucideIconNames = Object.keys(dynamicIconImports) as Array; diff --git a/packages/icons/src/icons/.gitkeep b/packages/icons/src/icons/.gitkeep new file mode 100644 index 000000000..e3d85f774 --- /dev/null +++ b/packages/icons/src/icons/.gitkeep @@ -0,0 +1 @@ +Folder for generated icons diff --git a/packages/icons/src/lucide-icons.prefixed.ts b/packages/icons/src/lucide-icons.prefixed.ts new file mode 100644 index 000000000..367f9b8ca --- /dev/null +++ b/packages/icons/src/lucide-icons.prefixed.ts @@ -0,0 +1,5 @@ +export * as icons from './icons'; +export * from './aliases/prefixed'; +export * from './types'; + +export * from './build'; diff --git a/packages/icons/src/lucide-icons.suffixed.ts b/packages/icons/src/lucide-icons.suffixed.ts new file mode 100644 index 000000000..551638884 --- /dev/null +++ b/packages/icons/src/lucide-icons.suffixed.ts @@ -0,0 +1,3 @@ +export * as icons from './icons'; +export * from './aliases/suffixed'; +export * from './types'; diff --git a/packages/icons/src/lucide-icons.ts b/packages/icons/src/lucide-icons.ts new file mode 100644 index 000000000..0d4146674 --- /dev/null +++ b/packages/icons/src/lucide-icons.ts @@ -0,0 +1,4 @@ +export * as icons from './icons'; +export * from './icons'; +export * from './aliases/index'; +export * from './types'; diff --git a/packages/icons/src/types.ts b/packages/icons/src/types.ts new file mode 100644 index 000000000..563c93fd7 --- /dev/null +++ b/packages/icons/src/types.ts @@ -0,0 +1,43 @@ +export type SVGProps = Record; + +/** + * A Lucide icon node (an svgson-like internal format) + */ +export type LucideIconNode = + | [attrName: string, attributes: SVGProps] + | [attrName: string, attributes: SVGProps, children: LucideIconNode[]]; + +/** + * A Lucide icon object that fully describes an icon to be displayed. + */ +export type LucideIconData = { + name: string; + node: LucideIconNode[]; + aliases?: string[]; +} & ({ size: number } | { width: number; height: number }); + +/** + * Build parameters for creating a Lucide icon instance for display. + */ +export type LucideBuildParams = { + /** + * The color of the icon. + */ + color?: string; + /** + * The stroke width. + */ + strokeWidth?: number; + /** + * Adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements. + */ + absoluteStrokeWidth?: boolean; + /** + * Extra CSS class names to pass to the SVG element. + */ + className?: string; + /** + * Any extra attributes to pass to the SVG element. + */ + attributes?: SVGProps; +} & ({ size?: number } | { width?: number; height?: number }); diff --git a/packages/icons/tests/__snapshots__/buildLucideIconElement.spec.ts.snap b/packages/icons/tests/__snapshots__/buildLucideIconElement.spec.ts.snap new file mode 100644 index 000000000..2963eecb8 --- /dev/null +++ b/packages/icons/tests/__snapshots__/buildLucideIconElement.spec.ts.snap @@ -0,0 +1,24 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`buildLucideIconElement > should match the snapshot 1`] = ` + + + + + + +`; diff --git a/packages/icons/tests/__snapshots__/buildLucideIconNode.spec.ts.snap b/packages/icons/tests/__snapshots__/buildLucideIconNode.spec.ts.snap new file mode 100644 index 000000000..75854cf7e --- /dev/null +++ b/packages/icons/tests/__snapshots__/buildLucideIconNode.spec.ts.snap @@ -0,0 +1,3 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`buildLucideIconNode > should match the snapshot 1`] = `"{"xmlns":"http://www.w3.org/2000/svg","width":"24","height":"24","viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","class":"lucide lucide-house lucide-home"}"`; diff --git a/packages/icons/tests/__snapshots__/buildLucideSvg.spec.ts.snap b/packages/icons/tests/__snapshots__/buildLucideSvg.spec.ts.snap new file mode 100644 index 000000000..97b586b69 --- /dev/null +++ b/packages/icons/tests/__snapshots__/buildLucideSvg.spec.ts.snap @@ -0,0 +1,26 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`buildLucideSvg > should match the snapshot 1`] = ` + + + + + + +`; + +exports[`buildLucideSvgDataUri > should match the snapshot 1`] = `"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWhvdXNlIGx1Y2lkZS1ob21lIj48cGF0aCBkPSJNMTUgMjF2LThhMSAxIDAgMCAwLTEtMWgtNGExIDEgMCAwIDAtMSAxdjgiIGtleT0iNXd3bHI1Ij48L3BhdGg+PHBhdGggZD0iTTMgMTBhMiAyIDAgMCAxIC43MDktMS41MjhsNy02YTIgMiAwIDAgMSAyLjU4MiAwbDcgNkEyIDIgMCAwIDEgMjEgMTB2OWEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMnoiIGtleT0icjZuc3MxIj48L3BhdGg+PC9zdmc+"`; diff --git a/packages/icons/tests/buildLucideIconElement.spec.ts b/packages/icons/tests/buildLucideIconElement.spec.ts new file mode 100644 index 000000000..88c9cc559 --- /dev/null +++ b/packages/icons/tests/buildLucideIconElement.spec.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { House } from '../src/lucide-icons'; +import { getOriginalSvg, removeKeys } from './helpers'; +import buildLucideIconElement from '../src/buildLucideIconElement'; + +describe('buildLucideIconElement', () => { + it('should create SVG Element', () => { + const HomeSVG = buildLucideIconElement(document, House); + + expect(HomeSVG.tagName).toBe('svg'); + }); + + it('should match the snapshot', () => { + const HomeSVG = buildLucideIconElement(document, House); + + expect(HomeSVG.outerHTML).toMatchSnapshot(); + }); + + it('should create SVG Element with attributes', () => { + const HomeSVG = buildLucideIconElement(document, House, { attributes: { fill: 'red' } }); + + expect(HomeSVG.getAttribute('fill')).toBe('red'); + }); + + it('should create SVG Element with class name', () => { + const HomeSVG = buildLucideIconElement(document, House, { attributes: { class: 'icon' } }); + + expect(HomeSVG.getAttribute('class')).toBe('icon'); + }); + + it('should add backwards-compatible classes', () => { + const HomeSVG = buildLucideIconElement(document, House); + + expect(HomeSVG.getAttribute('class')).toBe('lucide lucide-house lucide-home'); + }); + + it('should merge classes', () => { + const HomeSVG = buildLucideIconElement(document, House, { className: 'icon' }); + + expect(HomeSVG.getAttribute('class')).toBe('lucide lucide-house lucide-home icon'); + }); + + it('should create the correct svg element', () => { + const HomeSVG = buildLucideIconElement(document, House); + + const svg = getOriginalSvg('house', ['home']); + + expect(removeKeys(HomeSVG.outerHTML)).toBe(svg); + }); +}); diff --git a/packages/icons/tests/buildLucideIconNode.spec.ts b/packages/icons/tests/buildLucideIconNode.spec.ts new file mode 100644 index 000000000..dc3345ac5 --- /dev/null +++ b/packages/icons/tests/buildLucideIconNode.spec.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest'; +import { House } from '../src/lucide-icons'; +import buildLucideIconNode from '../src/buildLucideIconNode'; + +describe('buildLucideIconNode', () => { + it('should create icon node', () => { + const HouseSVG = buildLucideIconNode(House); + + expect(HouseSVG.at(0)).toBe('svg'); + }); + + it('should match the snapshot', () => { + const HouseSVG = buildLucideIconNode(House); + + expect(JSON.stringify(HouseSVG.at(1))).toMatchSnapshot(); + }); + + it('should override dimensions, but not viewBox', () => { + const HouseSVG = buildLucideIconNode(House, { size: 12 }); + + expect(HouseSVG[1]['width']).toBe('12'); + expect(HouseSVG[1]['height']).toBe('12'); + expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24'); + }); + + it('should override width, but not height', () => { + const HouseSVG = buildLucideIconNode(House, { width: 12 }); + + expect(HouseSVG[1]['width']).toBe('12'); + expect(HouseSVG[1]['height']).toBe('24'); + expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24'); + }); + + it('should override height, but not width', () => { + const HouseSVG = buildLucideIconNode(House, { height: 12 }); + + expect(HouseSVG[1]['width']).toBe('24'); + expect(HouseSVG[1]['height']).toBe('12'); + expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24'); + }); + + it('should override color', () => { + const HouseSVG = buildLucideIconNode(House, { color: 'pink' }); + + expect(HouseSVG[1]['stroke']).toBe('pink'); + expect(HouseSVG[1]['fill']).toBe('none'); + }); + + it('should override stroke width', () => { + const HouseSVG = buildLucideIconNode(House, { strokeWidth: 12 }); + + expect(HouseSVG[1]['stroke-width']).toBe('12'); + }); + + it('should set non-scaling-stroke to child nodes', () => { + const HouseSVG = buildLucideIconNode(House, { absoluteStrokeWidth: true }); + + for (const node of HouseSVG[2]!) { + expect(node[1]['vector-effect']).toBe('non-scaling-stroke'); + } + }); + + it('should not set non-scaling-stroke', () => { + const HouseSVG = buildLucideIconNode(House, { absoluteStrokeWidth: false }); + + expect(HouseSVG[1]['vector-effect']).toBeUndefined(); + }); + + it('should create icon node with attributes', () => { + const HouseSVG = buildLucideIconNode(House, { attributes: { fill: 'red' } }); + + expect(HouseSVG[1]['fill']).toBe('red'); + }); + + it('should create icon node with class name', () => { + const HouseSVG = buildLucideIconNode(House, { attributes: { class: 'icon' } }); + + expect(HouseSVG[1]['class']).toBe('icon'); + }); + + it('should add backwards-compatible classes', () => { + const HouseSVG = buildLucideIconNode(House); + + expect(HouseSVG[1]['class']).toBe('lucide lucide-house lucide-home'); + }); + + it('should merge classes', () => { + const HouseSVG = buildLucideIconNode(House, { className: 'icon' }); + + expect(HouseSVG[1]['class']).toBe('lucide lucide-house lucide-home icon'); + }); +}); diff --git a/packages/icons/tests/buildLucideSvg.spec.ts b/packages/icons/tests/buildLucideSvg.spec.ts new file mode 100644 index 000000000..7858aa732 --- /dev/null +++ b/packages/icons/tests/buildLucideSvg.spec.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import { House } from '../src/lucide-icons'; +import { getOriginalSvg, removeKeys } from './helpers'; +import buildLucideSvg from '../src/buildLucideSvg'; +import buildLucideDataUri from '../src/buildLucideDataUri'; + +describe('buildLucideSvg', () => { + it('should match the snapshot', () => { + const HouseSVG = buildLucideSvg(House); + + expect(HouseSVG).toMatchSnapshot(); + }); + + it('should create the correct svg element', () => { + const HouseSVG = buildLucideSvg(House); + + const svg = getOriginalSvg('house', ['home']); + + expect(removeKeys(HouseSVG)).toBe(svg); + }); +}); + +describe('buildLucideSvgDataUri', () => { + it('should match the snapshot', () => { + const HouseDataUri = buildLucideDataUri(House); + + expect(HouseDataUri).toMatchSnapshot(); + }); +}); diff --git a/packages/icons/tests/helpers.ts b/packages/icons/tests/helpers.ts new file mode 100644 index 000000000..b684d99dd --- /dev/null +++ b/packages/icons/tests/helpers.ts @@ -0,0 +1,19 @@ +import fs from 'fs'; +import path from 'path'; +import { parseSync, stringify } from 'svgson'; + +const ICONS_DIR = path.resolve(__dirname, '../../../icons'); + +export const getOriginalSvg = (iconName: string, aliasNames?: string[]) => { + const svgContent = fs.readFileSync(path.join(ICONS_DIR, `${iconName}.svg`), 'utf8'); + const svgParsed = parseSync(svgContent); + + if (aliasNames) { + svgParsed.attributes['class'] = + `lucide lucide-${iconName} ${aliasNames.map((alias) => `lucide-${alias}`).join(' ')}`; + } + + return stringify(svgParsed, { selfClose: false }); +}; + +export const removeKeys = (svg: string) => svg.replaceAll(/ key="[^"]+"/g, ''); diff --git a/packages/icons/tests/lucide-icons.spec.ts b/packages/icons/tests/lucide-icons.spec.ts new file mode 100644 index 000000000..6658b3988 --- /dev/null +++ b/packages/icons/tests/lucide-icons.spec.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest'; +import { House } from '../src/lucide-icons'; + +describe('lucide-icons', () => { + it('should init', () => { + const HouseSVG = House; + + expect(House).toMatchObject(House); + }); +}); diff --git a/packages/icons/tests/setupVitest.js b/packages/icons/tests/setupVitest.js new file mode 100644 index 000000000..03a0244af --- /dev/null +++ b/packages/icons/tests/setupVitest.js @@ -0,0 +1,5 @@ +import { expect } from 'vitest'; +import '@testing-library/jest-dom/vitest'; +import htmlSerializer from 'jest-serializer-html'; + +expect.addSnapshotSerializer(htmlSerializer); diff --git a/packages/icons/tsconfig.json b/packages/icons/tsconfig.json new file mode 100644 index 000000000..2e5db9189 --- /dev/null +++ b/packages/icons/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "strict": true, + "declaration": false, + "noEmitOnError": true, + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "module": "ESNext", + "target": "ESNext", + "esModuleInterop": true, + "isolatedModules": true, + "lib": ["esnext", "dom"], + "skipLibCheck": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "downlevelIteration": true, + "sourceMap": true, + "outDir": "./dist", + }, + "exclude": ["**/node_modules"], +} diff --git a/packages/icons/vitest.config.mts b/packages/icons/vitest.config.mts new file mode 100644 index 000000000..295f35b09 --- /dev/null +++ b/packages/icons/vitest.config.mts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + setupFiles: './tests/setupVitest.js', + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 313f353e0..3e413aa91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,6 +246,45 @@ importers: specifier: ^4.0.12 version: 4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(jsdom@20.0.3)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0) + packages/icons: + devDependencies: + '@lucide/build-icons': + specifier: workspace:* + version: link:../../tools/build-icons + '@lucide/helpers': + specifier: ^1.0.0 + version: 1.0.0 + '@lucide/rollup-plugins': + specifier: workspace:* + version: link:../../tools/rollup-plugins + '@rollup/plugin-replace': + specifier: ^6.0.2 + version: 6.0.3(rollup@4.59.0) + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.9.1 + jest-serializer-html: + specifier: ^7.1.0 + version: 7.1.0 + rollup: + specifier: ^4.53.3 + version: 4.59.0 + rollup-plugin-dts: + specifier: ^6.2.3 + version: 6.2.3(rollup@4.59.0)(typescript@5.9.3) + rollup-plugin-preserve-directives: + specifier: ^0.4.0 + version: 0.4.0(rollup@4.59.0) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vite: + specifier: ^7.2.4 + version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0) + vitest: + specifier: ^4.0.12 + version: 4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(jsdom@20.0.3)(less@4.2.0)(sass@1.77.8)(stylus@0.56.0)(terser@5.44.1)(yaml@2.8.0) + packages/lucide: devDependencies: '@lucide/build-icons': @@ -3849,6 +3888,9 @@ packages: '@lezer/lr@1.4.3': resolution: {integrity: sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==} + '@lucide/helpers@1.0.0': + resolution: {integrity: sha512-9dfxrgLLaoCfr3R/eh6wwlUcY+ZPdEv6SDwFMUhYoO6HhGL8yN8hb5ZwI/OfzbK9mdJpa+jYfwP4nF4ZlZwZLA==} + '@mapbox/node-pre-gyp@2.0.3': resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} engines: {node: '>=18'} @@ -4547,15 +4589,6 @@ packages: rollup: optional: true - '@rollup/pluginutils@5.1.0': - resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -9588,9 +9621,6 @@ packages: resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==} engines: {node: '>=12'} - magic-string@0.30.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -17134,6 +17164,8 @@ snapshots: dependencies: '@lezer/common': 1.3.0 + '@lucide/helpers@1.0.0': {} + '@mapbox/node-pre-gyp@2.0.3(encoding@0.1.13)': dependencies: consola: 3.4.2 @@ -17921,14 +17953,6 @@ snapshots: optionalDependencies: rollup: 4.59.0 - '@rollup/pluginutils@5.1.0(rollup@4.59.0)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 2.3.1 - optionalDependencies: - rollup: 4.59.0 - '@rollup/pluginutils@5.3.0(rollup@4.59.0)': dependencies: '@types/estree': 1.0.8 @@ -23770,10 +23794,6 @@ snapshots: dependencies: sourcemap-codec: 1.4.8 - magic-string@0.30.11: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -26509,8 +26529,8 @@ snapshots: rollup-plugin-preserve-directives@0.4.0(rollup@4.59.0): dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.59.0) - magic-string: 0.30.11 + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + magic-string: 0.30.21 rollup: 4.59.0 rollup-plugin-sourcemaps@0.6.3(@types/node@12.20.55)(rollup@2.79.2): diff --git a/tools/build-icons/building/generateIconFiles.ts b/tools/build-icons/building/generateIconFiles.ts index 9babf1cf2..b401fa2bd 100644 --- a/tools/build-icons/building/generateIconFiles.ts +++ b/tools/build-icons/building/generateIconFiles.ts @@ -48,15 +48,27 @@ function generateIconFiles({ ]); const getSvg = () => readSvg(`${iconName}.svg`, iconsDir); - const { deprecated = false, toBeRemovedInVersion = undefined } = iconMetaData[iconName]; + const { + deprecated = false, + toBeRemovedInVersion = undefined, + aliases = [], + } = iconMetaData[iconName]; const deprecationReason = deprecated - ? deprecationReasonTemplate(iconMetaData[iconName]?.deprecationReason ?? '', { + ? deprecationReasonTemplate(iconMetaData[iconName].deprecationReason ?? '', { componentName, iconName, toBeRemovedInVersion, }) : ''; + const iconData = { + name: iconName, + size: 24, + node: children, + ...((aliases?.length ?? 0) > 0 && { + aliases: aliases.map((alias) => (typeof alias === 'string' ? alias : alias.name)), + }), + }; const elementTemplate = await template({ componentName, iconName, @@ -64,6 +76,7 @@ function generateIconFiles({ getSvg, deprecated, deprecationReason, + iconData, }); const output = pretty diff --git a/tools/build-icons/types.ts b/tools/build-icons/types.ts index 7656f5ecc..a12b3ca75 100644 --- a/tools/build-icons/types.ts +++ b/tools/build-icons/types.ts @@ -6,14 +6,24 @@ export type IconNode = [tag: string, attrs: SVGProps][]; export type IconNodeWithChildren = [tag: string, attrs: SVGProps, children: IconNode]; -export type TemplateFunction = (params: { +export type IconData = { + name: string; + node: IconNode; + aliases?: string[]; +} & ({ size: number } | { width: number; height: number }); + +export interface ExportTemplate { componentName: string; iconName: string; children: IconNode; getSvg: () => Promise; - deprecated?: boolean; - deprecationReason?: string; -}) => Promise; + deprecated: boolean; + deprecationReason: string; + aliases?: (string | AliasDeprecation)[]; + iconData: IconData; +} + +export type TemplateFunction = (params: ExportTemplate) => Promise; export type Path = string; diff --git a/tools/build-icons/utils/defineExportTemplate.ts b/tools/build-icons/utils/defineExportTemplate.ts index 201937439..87b521152 100644 --- a/tools/build-icons/utils/defineExportTemplate.ts +++ b/tools/build-icons/utils/defineExportTemplate.ts @@ -1,15 +1,4 @@ -import { type IconNode } from '../types.ts'; - -export interface ExportTemplate { - componentName: string; - iconName: string; - children: IconNode; - getSvg: () => Promise; - deprecated: boolean; - deprecationReason: string; -} - -export type TemplateFunction = (params: ExportTemplate) => Promise; +import type { TemplateFunction } from '../types.ts'; const defineExportTemplate = (exportFunction: TemplateFunction) => exportFunction; diff --git a/tools/build-icons/utils/deprecationReasonTemplate.ts b/tools/build-icons/utils/deprecationReasonTemplate.ts index a82c2e7da..a8c2c8e4e 100644 --- a/tools/build-icons/utils/deprecationReasonTemplate.ts +++ b/tools/build-icons/utils/deprecationReasonTemplate.ts @@ -11,10 +11,9 @@ export default function deprecationReasonTemplate( toBeRemovedInVersion?: string; }, ) { + const resourceName = deprecationReason.startsWith('icon') ? 'icon' : 'alias'; const removalNotice = toBeRemovedInVersion - ? ` This ${ - deprecationReason.startsWith('icon') ? 'icon' : 'alias' - } will be removed in ${toBeRemovedInVersion}` + ? ` This ${resourceName} will be removed in ${toBeRemovedInVersion}` : ''; switch (deprecationReason) { @@ -24,6 +23,7 @@ export default function deprecationReasonTemplate( return `The icon was combined with another icon that shares the same use case, use {@link ${componentName}} instead.${removalNotice}`; case 'alias.name': return `The name of this icon was changed because it didn't meet our guidelines anymore, use {@link ${componentName}} instead.${removalNotice}`; + default: throw new Error(`Unknown deprecation reason: ${deprecationReason}`); } }