fix(lucide-react-native): Add icon exports property to the package.json file (#4522)

* Add export per icon

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Add documentations

* Format code

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Eric Fennis
2026-07-09 14:53:06 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 4c663b9a32
commit 7291cbcc0f
10 changed files with 137 additions and 13 deletions
+5
View File
@@ -72,6 +72,11 @@ export const reactNativeSidebar = [
link: '/guide/react-native/advanced/combining-icons',
desc: 'Combine multiple icons into one',
},
{
text: 'Optimizations',
link: '/guide/react-native/advanced/optimizations',
desc: 'Optimizing your app for performance',
},
],
},
{
@@ -0,0 +1,56 @@
---
title: Optimizations - React Native
description: Learn how to optimize the bundle size of your React Native and Expo web exports by tree-shaking Lucide icons.
---
# Optimizations
By default you import icons from the `lucide-react-native` entry point:
```jsx
import { Camera } from 'lucide-react-native';
```
This is convenient, but it relies on the bundler to _tree-shake_ the entry point (a "barrel" file that re-exports every icon) so that only the icons you actually use end up in your bundle.
On native platforms this is usually fine. On **web exports** it can be a problem, because the Metro bundler is currently not very good at tree-shaking barrel imports and may include **all** icons in your final bundle.
## The Metro tree-shaking limitation
When you build for web with Expo (`expo export --platform web`), Metro is used to bundle your JavaScript. Metro bundles modules on-demand and, by default, does not remove unused exports from a barrel file.
This means an import like the one below can pull in the entire icon set, even though only a single icon is rendered:
```jsx
// May bundle every Lucide icon on web exports
import { Camera } from 'lucide-react-native';
```
Expo documents an experimental optimization for [React Native web imports](https://docs.expo.dev/guides/tree-shaking/#react-native-web-imports) and for [removing unused imports and exports](https://docs.expo.dev/guides/tree-shaking/#remove-unused-imports-and-exports). In practice this tree-shaking does **not** currently work reliably for `lucide-react-native` barrel imports, so the unused icons are not removed from web exports.
## Recommended: import icons individually
To guarantee that only the icons you use are bundled — regardless of the bundler's tree-shaking capabilities — import each icon directly from its own module:
```jsx
import Camera from 'lucide-react-native/icons/camera';
// Usage
const App = () => {
return <Camera />;
};
export default App;
```
Because each icon lives in its own file, the bundler only ever includes the icons you explicitly import. This keeps your web export small without depending on Metro's experimental tree-shaking.
The icon module name is the [kebab-case](https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case) version of the icon name. For example, the `ArrowRight` icon is imported from `lucide-react-native/icons/arrow-right`:
```jsx
import ArrowRight from 'lucide-react-native/icons/arrow-right';
```
::: tip
Individual icon imports work identically on native and web, so you can safely use this style across your whole project to keep bundles as small as possible on every platform.
:::
+14 -4
View File
@@ -25,20 +25,29 @@
"amdName": "lucide-react-native",
"main": "dist/cjs/lucide-react-native.js",
"module": "dist/esm/lucide-react-native.mjs",
"typings": "dist/lucide-react-native.d.ts",
"typings": "dist/types/lucide-react-native.d.ts",
"react-native": "dist/esm/lucide-react-native.mjs",
"exports": {
".": {
"types": "./dist/lucide-react-native.d.ts",
"types": "./dist/types/lucide-react-native.d.ts",
"react-native": "./dist/esm/lucide-react-native.mjs",
"import": "./dist/esm/lucide-react-native.mjs",
"browser": "./dist/esm/lucide-react-native.mjs",
"require": "./dist/cjs/lucide-react-native.js"
},
"./icons": {
"types": "./dist/icons.d.ts",
"types": "./dist/types/icons.d.ts",
"react-native": "./dist/esm/icons/index.mjs",
"import": "./dist/esm/icons/index.mjs",
"browser": "./dist/esm/icons/index.mjs",
"require": "./dist/cjs/icons/index.js"
},
"./icons/*": {
"types": "./dist/types/icons/*.d.ts",
"react-native": "./dist/esm/icons/*.mjs",
"import": "./dist/esm/icons/*.mjs",
"browser": "./dist/esm/icons/*.mjs",
"require": "./dist/cjs/icons/*.js"
}
},
"sideEffects": false,
@@ -49,13 +58,14 @@
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && 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.mts --renderUniqueKey --iconFileExtension=.ts --exportFileName=index.ts --withAliases --aliasesFileExtension=.ts",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --iconFileExtension=.ts --exportFileName=index.ts --withAliases --separateAliasesFile --separateAliasesFileIgnore=fingerprint --aliasesFileExtension=.ts",
"build:bundles": "rollup -c ./rollup.config.mjs",
"test": "pnpm build:icons && vitest run",
"test:watch": "vitest watch"
},
"devDependencies": {
"@lucide/build-icons": "workspace:*",
"@lucide/helpers": "workspace:*",
"@lucide/rollup-plugins": "workspace:*",
"@lucide/shared": "workspace:*",
"@testing-library/jest-dom": "^6.8.0",
+23 -4
View File
@@ -1,6 +1,7 @@
import plugins from '@lucide/rollup-plugins';
import dts from 'rollup-plugin-dts';
import pkg from './package.json' with { type: 'json' };
import getIconEntryNamesAndAliases from './scripts/getIconEntryNamesAndAliases.mts';
const packageName = 'LucideReact';
const outputFileName = 'lucide-react-native';
@@ -22,6 +23,9 @@ const bundles = [
},
];
const iconAndAliasEntries = await getIconEntryNamesAndAliases();
const typesModuleMatcher = /[/\\]src[/\\]types\.(d\.)?ts$/;
const configs = bundles.map(({ inputs, outputDir, format, preserveModules, extension = 'js' }) => ({
input: inputs,
plugins: plugins({ pkg }),
@@ -52,7 +56,7 @@ export default [
input: inputs[0],
output: [
{
file: `dist/${outputFileName}.d.ts`,
file: `dist/types/${outputFileName}.d.ts`,
format: 'es',
},
],
@@ -62,17 +66,32 @@ export default [
input: 'src/icons/index.ts',
output: [
{
file: `dist/icons.d.ts`,
file: `dist/types/icons.d.ts`,
format: 'es',
},
],
plugins: [dts()],
},
{
input: iconAndAliasEntries,
// Treat `src/types.ts` as external so `LucideProps` isn't bundled into a shared
// hashed chunk; `output.paths` then rewrites the import to the main entry.
external: [typesModuleMatcher],
output: [
{
format: 'es',
dir: 'dist/types/icons',
entryFileNames: '[name].d.ts',
paths: (id) => (typesModuleMatcher.test(id) ? `../${outputFileName}.js` : id),
},
],
plugins: [dts()],
},
{
input: `src/${outputFileName}.suffixed.ts`,
output: [
{
file: `dist/${outputFileName}.suffixed.d.ts`,
file: `dist/types/${outputFileName}.suffixed.d.ts`,
format: 'es',
},
],
@@ -82,7 +101,7 @@ export default [
input: `src/${outputFileName}.prefixed.ts`,
output: [
{
file: `dist/${outputFileName}.prefixed.d.ts`,
file: `dist/types/${outputFileName}.prefixed.d.ts`,
format: 'es',
},
],
@@ -0,0 +1,27 @@
import path from 'path';
import { readSvgDirectory } from '@lucide/helpers';
import getIconMetaData from '@lucide/build-icons/utils/getIconMetaData';
const ICONS_DIR = path.resolve(process.cwd(), '../../icons');
const ignoreAliases = ['fingerprint'];
export default async function getIconEntryNamesAndAliases() {
const svgFiles = await readSvgDirectory(ICONS_DIR);
const iconNames = svgFiles.map((file) => path.basename(file, '.svg'));
const metaJsonFiles = await getIconMetaData(ICONS_DIR);
const iconWithAliases = Object.values(metaJsonFiles).filter(({ aliases }) => aliases != null);
const aliases = iconWithAliases.flatMap(({ aliases }) => aliases ?? []);
const aliasNames = aliases
.filter((alias) => !ignoreAliases.includes(alias.name))
.map((alias) => alias.name);
// Return an object mapping the entry name to its source path so Rollup uses the
// key as the `[name]` placeholder and emits declaration files flat in the output
// directory instead of preserving the `src/icons/` directory structure.
return Object.fromEntries(
[...iconNames, ...aliasNames].map((name) => [name, path.join('src/icons', `${name}.ts`)]),
);
}
+3
View File
@@ -511,6 +511,9 @@ importers:
'@lucide/build-icons':
specifier: workspace:*
version: link:../../tools/build-icons
'@lucide/helpers':
specifier: workspace:*
version: link:../../tools/build-helpers
'@lucide/rollup-plugins':
specifier: workspace:*
version: link:../../tools/rollup-plugins
+2 -2
View File
@@ -4,8 +4,8 @@
"declaration": true,
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"module": "ESNext",
"moduleResolution": "nodenext",
"module": "NodeNext",
"target": "ESNext",
"esModuleInterop": true,
"lib": ["esnext"],
+4
View File
@@ -25,6 +25,10 @@
"./utils/*": {
"import": "./utils/*.ts",
"require": "./utils/*.ts"
},
"./types": {
"types": "./types.ts",
"import": "./types.ts"
}
},
"keywords": [],
+2 -2
View File
@@ -4,8 +4,8 @@
"declaration": true,
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"module": "ESNext",
"moduleResolution": "nodenext",
"module": "NodeNext",
"target": "ESNext",
"esModuleInterop": true,
"lib": ["esnext"],
+1 -1
View File
@@ -40,7 +40,7 @@ export type IconDeprecationReason = 'icon.renamed' | '';
export type IconMetadataBase = {
toBeRemovedInVersion?: string;
categories: string[];
aliases?: (string | AliasDeprecation)[];
aliases?: AliasDeprecation[];
tags: string[];
deprecationReason?: IconDeprecationReason;
deprecated?: boolean;