Skip to content

Commit

Permalink
Improve types export for lucide-react (#1424)
Browse files Browse the repository at this point in the history
* Add types plugin

* Add js doc comment

* Only enable dynamic imports for CJS and ESM builds

* Add documentation

* Adjust docs

* Add test for dynamic import

* Adjust note

* Adjustment in docs
  • Loading branch information
ericfennis committed Jul 13, 2023
1 parent e9d69c6 commit c97c6ed
Show file tree
Hide file tree
Showing 15 changed files with 271 additions and 112 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ packages/**/src/icons/*.js
packages/**/src/icons/*.ts
packages/**/src/icons/*.tsx
packages/**/src/aliases.ts
packages/**/src/dynamicIconImports.ts
packages/**/LICENSE
categories.json
tags.json
Expand Down
58 changes: 58 additions & 0 deletions docs/guide/packages/lucide-react.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ It is possible to create one generic icon component to load icons. It's not reco

::: danger
Example below importing all ES Modules, caution using this example. All icons will be imported. When using bundlers like: `Webpack`, `Rollup` or `Vite` the application build size will grow strongly and harming the performance the application.

This is not the case for the latest NextJS, because it uses server side rendering. The icons will be streamed to the client when needed. For NextJS with Dynamic Imports, see [dynamic imports](#nextjs-example) section for more information.
:::

### Icon Component Example
Expand Down Expand Up @@ -94,3 +96,59 @@ const App = () => {

export default App;
```

#### With Dynamic Imports

> :warning: This is experimental and only works with bundlers that support dynamic imports.
Lucide react exports a dynamic import map `dynamicIconImports`. Useful for applications that want to show icons dynamically by icon name. For example when using a content management system with where icon names are stored in a database.

When using client side rendering, it will fetch the icon component when it's needed. This will reduce the initial bundle size.

The keys of the dynamic import map are the lucide original icon names (kebab case).

Example with React suspense:

```tsx
import React, { lazy, Suspense } from 'react';
import { dynamicIconImports, LucideProps } from 'lucide-react';

const fallback = <div style={{ background: '#ddd', width: 24, height: 24 }}/>

interface IconProps extends Omit<LucideProps, 'ref'> {
name: keyof typeof dynamicIconImports;
}

const Icon = ({ name, ...props }: IconProps) => {
const LucideIcon = lazy(dynamicIconImports[name]);

return (
<Suspense fallback={fallback}>
<LucideIcon {...props} />
</Suspense>
);
}

export default Icon
```

##### NextJS Example

In NextJS [the dynamic function](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic) can be used to load the icon component dynamically.

```tsx
import dynamic from 'next/dynamic'
import { dynamicIconImports, LucideProps } from 'lucide-react';

interface IconProps extends LucideProps {
name: keyof typeof dynamicIconImports;
}

const Icon = ({ name, ...props }: IconProps) => {
const LucideIcon = dynamic(dynamicIconImports[name])

return <LucideIcon {...props} />;
};

export default Icon;
```
58 changes: 57 additions & 1 deletion packages/lucide-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ It is possible to create a generic icon component to load icons.
> :warning: The example below is importing all ES modules. This is **not** recommended when you using a bundler since your application build size will grow substantially.
```js
import * as icons from 'lucide-react';
import { icons } from 'lucide-react';

const Icon = ({ name, color, size }) => {
const LucideIcon = icons[name];
Expand All @@ -70,3 +70,59 @@ const Icon = ({ name, color, size }) => {

export default Icon;
```

#### With Dynamic Imports

> :warning: This is experimental and only works with bundlers that support dynamic imports.
Lucide react exports a dynamic import map `dynamicIconImports`. Useful for applications that want to show icons dynamically by icon name. For example when using a content management system with where icon names are stored in a database.

When using client side rendering, it will fetch the icon component when it's needed. This will reduce the initial bundle size.

The keys of the dynamic import map are the lucide original icon names.

Example with React suspense:

```tsx
import React, { lazy, Suspense } from 'react';
import { dynamicIconImports, LucideProps } from 'lucide-react';

const fallback = <div style={{ background: '#ddd', width: 24, height: 24 }}/>

interface IconProps extends Omit<LucideProps, 'ref'> {
name: keyof typeof dynamicIconImports;
}

const Icon = ({ name, ...props }: IconProps) => {
const LucideIcon = lazy(dynamicIconImports[name]);

return (
<Suspense fallback={fallback}>
<LucideIcon {...props} />
</Suspense>
);
}

export default Icon
```

##### NextJS Example

In NextJS [the dynamic function](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic) can be used to load the icon component dynamically.

```tsx
import dynamic from 'next/dynamic'
import { dynamicIconImports, LucideProps } from 'lucide-react';

interface IconProps extends LucideProps {
name: keyof typeof dynamicIconImports;
}

const Icon = ({ name, ...props }: IconProps) => {
const LucideIcon = dynamic(dynamicIconImports[name])

return <LucideIcon {...props} />;
};

export default Icon;
```
5 changes: 3 additions & 2 deletions packages/lucide-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
"dist"
],
"scripts": {
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm typecheck && pnpm build:bundles && pnpm build:types",
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm typecheck && pnpm build:bundles",
"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 --aliasesFileExtension=.ts --iconFileExtension=.ts --exportFileName=index.ts",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mjs --renderUniqueKey --withAliases --withDynamicImports --aliasesFileExtension=.ts --iconFileExtension=.ts --exportFileName=index.ts",
"build:types": "node ./scripts/buildTypes.mjs",
"build:bundles": "rollup -c ./rollup.config.mjs",
"typecheck": "tsc",
Expand All @@ -44,6 +44,7 @@
"react": "17.0.2",
"react-dom": "17.0.2",
"rollup": "^3.5.1",
"rollup-plugin-dts": "^5.0.0",
"typescript": "^4.8.4",
"vite": "^4.3.9",
"vitest": "^0.32.2"
Expand Down
29 changes: 25 additions & 4 deletions packages/lucide-react/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import plugins, { replace } from '@lucide/rollup-plugins';
import pkg from './package.json' assert { type: 'json' };
import dts from "rollup-plugin-dts";

const packageName = 'LucideReact';
const outputFileName = 'lucide-react';
Expand All @@ -21,19 +22,21 @@ const bundles = [
format: 'cjs',
inputs,
outputDir,
aliasesSupport: true
aliasesSupport: true,
withDynamicImports: true,
},
{
format: 'esm',
inputs,
outputDir,
preserveModules: true,
aliasesSupport: true
aliasesSupport: true,
withDynamicImports: true,
},
];

const configs = bundles
.map(({ inputs, outputDir, format, minify, preserveModules, aliasesSupport }) =>
.map(({ inputs, outputDir, format, minify, preserveModules, aliasesSupport, withDynamicImports }) =>
inputs.map(input => ({
input,
plugins: [
Expand All @@ -47,6 +50,15 @@ const configs = bundles
}),
] : []
),
...(
!withDynamicImports ? [
replace({
"export { default as dynamicIconImports } from './dynamicIconImports';": '',
delimiters: ['', ''],
preventAssignment: false,
}),
] : []
),
...plugins(pkg, minify)
],
external: ['react', 'prop-types'],
Expand All @@ -71,4 +83,13 @@ const configs = bundles
)
.flat();

export default configs;
export default [
{
input: inputs[0],
output: [{
file: `dist/${outputFileName}.d.ts`, format: "es"
}],
plugins: [dts()],
},
...configs
];
92 changes: 0 additions & 92 deletions packages/lucide-react/scripts/buildTypes.mjs

This file was deleted.

28 changes: 27 additions & 1 deletion packages/lucide-react/scripts/exportTemplate.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
export default ({ componentName, children }) => `
export default ({ componentName, iconName, children, getSvg }) => {
const svgContents = getSvg();

const svgBase64 = Buffer.from(
svgContents
.replace('\n', '')
.replace(
'stroke="currentColor"',
'stroke="#000" style="background-color: #fff; border-radius: 2px"',
),
).toString('base64');

// declarationFileContent += `\

return `
import createLucideIcon from '../createLucideIcon';
/**
* @component @name ${componentName}
* @description Lucide SVG icon component, renders SVG Element with children.
*
* @preview ![img](data:image/svg+xml;base64,${svgBase64}) - https://lucide.dev/icons/${iconName}
* @see https://lucide.dev/guide/packages/lucide-react - Documentation
*
* @param {Object} props - Lucide icons props and any valid SVG attribute
* @returns {JSX.Element} JSX Element
*
*/
const ${componentName} = createLucideIcon('${componentName}', ${JSON.stringify(children)});
export default ${componentName};
`;
};
6 changes: 4 additions & 2 deletions packages/lucide-react/src/createLucideIcon.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { forwardRef, createElement, ReactSVG, SVGProps } from 'react';
import { forwardRef, createElement, ReactSVG, SVGProps, ForwardRefExoticComponent, RefAttributes } from 'react';
import defaultAttributes from './defaultAttributes';

export type IconNode = [elementName: keyof ReactSVG, attrs: Record<string, string>][]
Expand All @@ -9,6 +9,8 @@ export interface LucideProps extends SVGAttributes {
size?: string | number
absoluteStrokeWidth?: boolean
}

export type LucideIcon = ForwardRefExoticComponent<LucideProps & RefAttributes<SVGSVGElement>>
/**
* Converts string to KebabCase
* Copied from scripts/helper. If anyone knows how to properly import it here
Expand All @@ -19,7 +21,7 @@ export interface LucideProps extends SVGAttributes {
*/
export const toKebabCase = (string: string) => string.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();

const createLucideIcon = (iconName: string, iconNode: IconNode) => {
const createLucideIcon = (iconName: string, iconNode: IconNode): LucideIcon => {
const Component = forwardRef<SVGSVGElement, LucideProps>(
({ color = 'currentColor', size = 24, strokeWidth = 2, absoluteStrokeWidth, children, ...rest }, ref) =>
createElement(
Expand Down
8 changes: 7 additions & 1 deletion packages/lucide-react/src/lucide-react.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export * from './icons';
export * as icons from './icons';
export * from './aliases';
export { default as createLucideIcon } from './createLucideIcon';
export { default as dynamicIconImports } from './dynamicIconImports';
export {
default as createLucideIcon,
type IconNode,
type LucideProps,
type LucideIcon,
} from './createLucideIcon';
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
exports[`Using lucide icon components > should not scale the strokeWidth when absoluteStrokeWidth is set 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"48\\" height=\\"48\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"red\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"lucide lucide-grid\\" data-testid=\\"grid-icon\\"><rect width=\\"18\\" height=\\"18\\" x=\\"3\\" y=\\"3\\" rx=\\"2\\" ry=\\"2\\"></rect><line x1=\\"3\\" x2=\\"21\\" y1=\\"9\\" y2=\\"9\\"></line><line x1=\\"3\\" x2=\\"21\\" y1=\\"15\\" y2=\\"15\\"></line><line x1=\\"9\\" x2=\\"9\\" y1=\\"3\\" y2=\\"21\\"></line><line x1=\\"15\\" x2=\\"15\\" y1=\\"3\\" y2=\\"21\\"></line></svg>"`;
exports[`Using lucide icon components > should render an component 1`] = `"<svg 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-grid\\"><rect width=\\"18\\" height=\\"18\\" x=\\"3\\" y=\\"3\\" rx=\\"2\\" ry=\\"2\\"></rect><line x1=\\"3\\" x2=\\"21\\" y1=\\"9\\" y2=\\"9\\"></line><line x1=\\"3\\" x2=\\"21\\" y1=\\"15\\" y2=\\"15\\"></line><line x1=\\"9\\" x2=\\"9\\" y1=\\"3\\" y2=\\"21\\"></line><line x1=\\"15\\" x2=\\"15\\" y1=\\"3\\" y2=\\"21\\"></line></svg>"`;
exports[`Using lucide icon components > should render icons dynamically by using the dynamicIconImports module 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"48\\" height=\\"48\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"red\\" stroke-width=\\"1\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" class=\\"lucide lucide-smile\\" aria-label=\\"smile\\"><circle cx=\\"12\\" cy=\\"12\\" r=\\"10\\"></circle><path d=\\"M8 14s1.5 2 4 2 4-2 4-2\\"></path><line x1=\\"9\\" x2=\\"9.01\\" y1=\\"9\\" y2=\\"9\\"></line><line x1=\\"15\\" x2=\\"15.01\\" y1=\\"9\\" y2=\\"9\\"></line></svg>"`;
Loading

0 comments on commit c97c6ed

Please sign in to comment.