chore(lucide-react): Add Lucide React integration tests (#4804)

* Add Lucide React integration tests

* Apply batched suggestions from code review

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

* Remove redundant icon comparison test

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

* test(lucide-react): fix browser locator markup assertions

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Eric Fennis
2026-09-04 16:26:02 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
parent 93a1d72465
commit 07a2c792ef
58 changed files with 2978 additions and 183 deletions
+22
View File
@@ -7,10 +7,13 @@ on:
pull_request:
paths:
- packages/lucide-react/**
- integrations/lucide-react/**
- packages/shared/**
- tools/build-icons/**
- tools/rollup-plugins/**
- scripts/generateNextJSAliases.mjs
- package.json
- pnpm-workspace.yaml
- pnpm-lock.yaml
permissions:
@@ -48,3 +51,22 @@ jobs:
- name: Test
run: pnpm --filter lucide-react test
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Chromium
run: pnpm --filter @lucide/integration-react-vite exec playwright install --with-deps chromium
- name: Run integration checks
run: pnpm test:integrations:react
+5
View File
@@ -6,6 +6,7 @@
.cache
.env
node_modules
.pnpm-store
dist
build
sandbox
@@ -13,6 +14,10 @@ stash
coverage
stats
*.log
*.tsbuildinfo
integrations/**/.react-router
integrations/**/.vitest-attachments
integrations/**/__screenshots__
outlined
lucide-font
packages/**/src/icons/*.js
+7
View File
@@ -19,6 +19,13 @@ packages/lucide-static/icon-nodes.json
packages/lucide-static/font
packages/svelte/.svelte-kit
# integration test artifacts and generated routes
integrations/**/.react-router
integrations/**/next-env.d.ts
integrations/**/routeTree.gen.ts
integrations/**/.vitest-attachments
integrations/**/__screenshots__
# lucide-svelte
packages/svelte/src/icons/*.svelte
packages/svelte/.svelte-kit
+18
View File
@@ -0,0 +1,18 @@
# Lucide React integrations
These fixtures exercise the built `lucide-react` package in real consumer frameworks.
Every fixture runs three checks:
- a framework production build;
- TypeScript with `noEmit`;
- Vitest Browser Mode in Chromium.
From the repository root, install Chromium once and run the complete suite:
```sh
pnpm --filter @lucide/integration-react-vite exec playwright install chromium
pnpm test:integrations:react
```
The fixtures depend on `lucide-react` through `workspace:*`, but import only its public package entry points. The root command builds `lucide-react` before running any consumer checks.
@@ -0,0 +1,34 @@
'use client';
import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react';
import { DynamicIcon } from 'lucide-react/dynamic';
export default function IconShowcase() {
return (
<main aria-label="Lucide integration">
<Camera data-testid="static-icon" />
<Droplet
data-testid="custom-icon"
className="consumer-icon"
color="red"
size={48}
strokeWidth={4}
absoluteStrokeWidth
/>
<LucideProvider
color="purple"
size={32}
strokeWidth={3}
>
<House data-testid="provider-icon" />
</LucideProvider>
<Pen data-testid="alias-icon" />
<Edit2 data-testid="canonical-icon" />
<DynamicIcon
aria-label="Dynamic circle"
data-testid="dynamic-icon"
name="circle"
/>
</main>
);
}
@@ -0,0 +1,9 @@
import type { ReactNode } from 'react';
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
@@ -0,0 +1,5 @@
import IconShowcase from './IconShowcase';
export default function Page() {
return <IconShowcase />;
}
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,28 @@
{
"name": "@lucide/integration-react-nextjs",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "next build",
"typecheck": "next typegen && tsc --noEmit",
"test:browser": "vitest run",
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
},
"dependencies": {
"lucide-react": "workspace:*",
"next": "16.3.4",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@types/react": "19.2.18",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/browser-playwright": "4.1.10",
"typescript": "^6.0.3",
"vite": "^7.3.6",
"vitest": "4.1.10",
"vitest-browser-react": "2.2.0"
}
}
@@ -0,0 +1,38 @@
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-react';
import Page from '../app/page';
test('renders Lucide public APIs in a real browser', async () => {
const screen = await render(<Page />);
const staticIcon = screen.getByTestId('static-icon');
await expect.element(staticIcon).toBeVisible();
await expect.element(staticIcon).toHaveAttribute('width', '24');
await expect.element(staticIcon).toHaveAttribute('height', '24');
await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24');
await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true');
const customIcon = screen.getByTestId('custom-icon');
await expect.element(customIcon).toBeVisible();
await expect.element(customIcon).toHaveAttribute('width', '48');
await expect.element(customIcon).toHaveAttribute('height', '48');
await expect.element(customIcon).toHaveAttribute('stroke', 'red');
await expect.element(customIcon).toHaveAttribute('stroke-width', '2');
await expect.element(customIcon).toHaveClass('consumer-icon');
const providerIcon = screen.getByTestId('provider-icon');
await expect.element(providerIcon).toHaveAttribute('width', '32');
await expect.element(providerIcon).toHaveAttribute('stroke', 'purple');
await expect.element(providerIcon).toHaveAttribute('stroke-width', '3');
const aliasIcon = screen.getByTestId('alias-icon');
const canonicalIcon = screen.getByTestId('canonical-icon');
await expect.element(aliasIcon).toBeVisible();
await expect.element(canonicalIcon).toBeVisible();
expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML);
const dynamicIcon = screen.getByTestId('dynamic-icon');
await expect.element(dynamicIcon).toBeVisible();
await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle');
await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden');
});
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [{ "name": "next" }],
"types": ["vitest/globals"],
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
],
"exclude": ["node_modules"],
}
@@ -0,0 +1,15 @@
import react from '@vitejs/plugin-react';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
});
@@ -0,0 +1,32 @@
import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react';
import { DynamicIcon } from 'lucide-react/dynamic';
export default function IconShowcase() {
return (
<main aria-label="Lucide integration">
<Camera data-testid="static-icon" />
<Droplet
data-testid="custom-icon"
className="consumer-icon"
color="red"
size={48}
strokeWidth={4}
absoluteStrokeWidth
/>
<LucideProvider
color="purple"
size={32}
strokeWidth={3}
>
<House data-testid="provider-icon" />
</LucideProvider>
<Pen data-testid="alias-icon" />
<Edit2 data-testid="canonical-icon" />
<DynamicIcon
aria-label="Dynamic circle"
data-testid="dynamic-icon"
name="circle"
/>
</main>
);
}
@@ -0,0 +1,27 @@
import type { ReactNode } from 'react';
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router';
export function Layout({ children }: Readonly<{ children: ReactNode }>) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
content="width=device-width, initial-scale=1"
name="viewport"
/>
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
@@ -0,0 +1,6 @@
import { index, route, type RouteConfig } from '@react-router/dev/routes';
export default [
index('./routes/home.tsx'),
route('about', './routes/about.tsx'),
] satisfies RouteConfig;
@@ -0,0 +1,11 @@
import { ArrowLeft } from 'lucide-react';
import { Link } from 'react-router';
export default function About() {
return (
<main>
<ArrowLeft data-testid="route-icon" />
<Link to="/">Home</Link>
</main>
);
}
@@ -0,0 +1,11 @@
import { Link } from 'react-router';
import IconShowcase from '../IconShowcase';
export default function Home() {
return (
<>
<IconShowcase />
<Link to="/about">About</Link>
</>
);
}
@@ -0,0 +1,33 @@
{
"name": "@lucide/integration-react-react-router",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "react-router build",
"typecheck": "react-router typegen && tsc --noEmit",
"test:browser": "vitest run",
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
},
"dependencies": {
"@react-router/node": "8.3.1",
"@react-router/serve": "8.3.1",
"isbot": "5.2.2",
"lucide-react": "workspace:*",
"react": "19.2.8",
"react-dom": "19.2.8",
"react-router": "8.3.1"
},
"devDependencies": {
"@react-router/dev": "8.3.1",
"@types/node": "^26.1.1",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/browser-playwright": "4.1.10",
"typescript": "^6.0.3",
"vite": "^7.3.6",
"vitest": "4.1.10",
"vitest-browser-react": "2.2.0"
}
}
@@ -0,0 +1,5 @@
import type { Config } from '@react-router/dev/config';
export default {
ssr: true,
} satisfies Config;
@@ -0,0 +1,52 @@
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-react';
import { MemoryRouter, Route, Routes } from 'react-router';
import About from '../app/routes/about';
import Home from '../app/routes/home';
test('renders Lucide public APIs and survives router navigation', async () => {
const screen = await render(
<MemoryRouter initialEntries={['/']}>
<Routes>
<Route
element={<Home />}
path="/"
/>
<Route
element={<About />}
path="/about"
/>
</Routes>
</MemoryRouter>,
);
const staticIcon = screen.getByTestId('static-icon');
await expect.element(staticIcon).toBeVisible();
await expect.element(staticIcon).toHaveAttribute('width', '24');
await expect.element(staticIcon).toHaveAttribute('height', '24');
await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24');
await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true');
const customIcon = screen.getByTestId('custom-icon');
await expect.element(customIcon).toBeVisible();
await expect.element(customIcon).toHaveAttribute('width', '48');
const aliasIcon = screen.getByTestId('alias-icon');
const canonicalIcon = screen.getByTestId('canonical-icon');
await expect.element(aliasIcon).toBeVisible();
await expect.element(canonicalIcon).toBeVisible();
expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML);
await expect.element(customIcon).toHaveClass('consumer-icon');
const providerIcon = screen.getByTestId('provider-icon');
await expect.element(providerIcon).toHaveAttribute('width', '32');
await expect.element(providerIcon).toHaveAttribute('stroke', 'purple');
await expect.element(providerIcon).toHaveAttribute('stroke-width', '3');
const dynamicIcon = screen.getByTestId('dynamic-icon');
await expect.element(dynamicIcon).toBeVisible();
await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle');
await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden');
await screen.getByRole('link', { name: 'About' }).click();
await expect.element(screen.getByTestId('route-icon')).toBeVisible();
});
@@ -0,0 +1,18 @@
{
"include": ["**/*", ".react-router/types/**/*"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client", "vitest/globals"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"rootDirs": [".", "./.react-router/types"],
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
},
}
@@ -0,0 +1,6 @@
import { reactRouter } from '@react-router/dev/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [reactRouter()],
});
@@ -0,0 +1,15 @@
import react from '@vitejs/plugin-react';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
});
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Lucide React TanStack Router integration</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
@@ -0,0 +1,29 @@
{
"name": "@lucide/integration-react-tanstack-router",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit",
"test:browser": "vitest run",
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
},
"dependencies": {
"@tanstack/react-router": "1.170.32",
"lucide-react": "workspace:*",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@tanstack/router-plugin": "1.168.35",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/browser-playwright": "4.1.10",
"typescript": "^6.0.3",
"vite": "^7.3.6",
"vitest": "4.1.10",
"vitest-browser-react": "2.2.0"
}
}
@@ -0,0 +1,32 @@
import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react';
import { DynamicIcon } from 'lucide-react/dynamic';
export default function IconShowcase() {
return (
<main aria-label="Lucide integration">
<Camera data-testid="static-icon" />
<Droplet
data-testid="custom-icon"
className="consumer-icon"
color="red"
size={48}
strokeWidth={4}
absoluteStrokeWidth
/>
<LucideProvider
color="purple"
size={32}
strokeWidth={3}
>
<House data-testid="provider-icon" />
</LucideProvider>
<Pen data-testid="alias-icon" />
<Edit2 data-testid="canonical-icon" />
<DynamicIcon
aria-label="Dynamic circle"
data-testid="dynamic-icon"
name="circle"
/>
</main>
);
}
@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from '@tanstack/react-router';
import { router } from './router';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
);
@@ -0,0 +1,77 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AboutRouteImport } from './routes/about'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AboutRoute = AboutRouteImport.update({
id: '/about',
path: '/about',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/about': typeof AboutRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/about': typeof AboutRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/about': typeof AboutRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/about'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/about'
id: '__root__' | '/' | '/about'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/about': {
id: '/about'
path: '/about'
fullPath: '/about'
preLoaderRoute: typeof AboutRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AboutRoute: AboutRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
@@ -0,0 +1,10 @@
import { createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
export const router = createRouter({ routeTree });
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
@@ -0,0 +1,5 @@
import { Outlet, createRootRoute } from '@tanstack/react-router';
export const Route = createRootRoute({
component: () => <Outlet />,
});
@@ -0,0 +1,15 @@
import { Link, createFileRoute } from '@tanstack/react-router';
import { ArrowLeft } from 'lucide-react';
export const Route = createFileRoute('/about')({
component: About,
});
function About() {
return (
<main>
<ArrowLeft data-testid="route-icon" />
<Link to="/">Home</Link>
</main>
);
}
@@ -0,0 +1,15 @@
import { Link, createFileRoute } from '@tanstack/react-router';
import IconShowcase from '../IconShowcase';
export const Route = createFileRoute('/')({
component: Home,
});
function Home() {
return (
<>
<IconShowcase />
<Link to="/about">About</Link>
</>
);
}
@@ -0,0 +1,46 @@
import { createMemoryHistory, createRouter, RouterProvider } from '@tanstack/react-router';
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-react';
import { routeTree } from '../src/routeTree.gen';
test('renders Lucide public APIs and survives router navigation', async () => {
const router = createRouter({
history: createMemoryHistory({ initialEntries: ['/'] }),
routeTree,
});
const screen = await render(<RouterProvider router={router} />);
const staticIcon = screen.getByTestId('static-icon');
await expect.element(staticIcon).toBeVisible();
await expect.element(staticIcon).toHaveAttribute('width', '24');
await expect.element(staticIcon).toHaveAttribute('height', '24');
await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24');
await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true');
const customIcon = screen.getByTestId('custom-icon');
await expect.element(customIcon).toBeVisible();
await expect.element(customIcon).toHaveAttribute('width', '48');
await expect.element(customIcon).toHaveAttribute('height', '48');
await expect.element(customIcon).toHaveAttribute('stroke', 'red');
await expect.element(customIcon).toHaveAttribute('stroke-width', '2');
await expect.element(customIcon).toHaveClass('consumer-icon');
const providerIcon = screen.getByTestId('provider-icon');
await expect.element(providerIcon).toHaveAttribute('width', '32');
await expect.element(providerIcon).toHaveAttribute('stroke', 'purple');
await expect.element(providerIcon).toHaveAttribute('stroke-width', '3');
const aliasIcon = screen.getByTestId('alias-icon');
const canonicalIcon = screen.getByTestId('canonical-icon');
await expect.element(aliasIcon).toBeVisible();
await expect.element(canonicalIcon).toBeVisible();
expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML);
const dynamicIcon = screen.getByTestId('dynamic-icon');
await expect.element(dynamicIcon).toBeVisible();
await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle');
await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden');
await screen.getByRole('link', { name: 'About' }).click();
await expect.element(screen.getByTestId('route-icon')).toBeVisible();
});
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["vite/client", "vitest/globals"],
},
"include": ["src", "tests", "vite.config.ts", "vitest.config.ts"],
}
@@ -0,0 +1,13 @@
import react from '@vitejs/plugin-react';
import { tanstackRouter } from '@tanstack/router-plugin/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
tanstackRouter({
target: 'react',
autoCodeSplitting: true,
}),
react(),
],
});
@@ -0,0 +1,17 @@
import { playwright } from '@vitest/browser-playwright';
import { defineConfig, mergeConfig } from 'vitest/config';
import viteConfig from './vite.config';
export default mergeConfig(
viteConfig,
defineConfig({
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
}),
);
@@ -0,0 +1,30 @@
{
"name": "@lucide/integration-react-tanstack-start",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit",
"test:browser": "vitest run",
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
},
"dependencies": {
"@tanstack/react-router": "1.170.32",
"@tanstack/react-start": "1.168.49",
"lucide-react": "workspace:*",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@types/node": "^26.1.1",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/browser-playwright": "4.1.10",
"typescript": "^6.0.3",
"vite": "^7.3.6",
"vitest": "4.1.10",
"vitest-browser-react": "2.2.0"
}
}
@@ -0,0 +1,32 @@
import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react';
import { DynamicIcon } from 'lucide-react/dynamic';
export default function IconShowcase() {
return (
<main aria-label="Lucide integration">
<Camera data-testid="static-icon" />
<Droplet
data-testid="custom-icon"
className="consumer-icon"
color="red"
size={48}
strokeWidth={4}
absoluteStrokeWidth
/>
<LucideProvider
color="purple"
size={32}
strokeWidth={3}
>
<House data-testid="provider-icon" />
</LucideProvider>
<Pen data-testid="alias-icon" />
<Edit2 data-testid="canonical-icon" />
<DynamicIcon
aria-label="Dynamic circle"
data-testid="dynamic-icon"
name="circle"
/>
</main>
);
}
@@ -0,0 +1,86 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AboutRouteImport } from './routes/about'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const AboutRoute = AboutRouteImport.update({
id: '/about',
path: '/about',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/about': typeof AboutRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/about': typeof AboutRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/about': typeof AboutRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/about'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/about'
id: '__root__' | '/' | '/about'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/about': {
id: '/about'
path: '/about'
fullPath: '/about'
preLoaderRoute: typeof AboutRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AboutRoute: AboutRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
@@ -0,0 +1,15 @@
import { createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
export function getRouter() {
return createRouter({
routeTree,
scrollRestoration: true,
});
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>;
}
}
@@ -0,0 +1,35 @@
import type { ReactNode } from 'react';
import { HeadContent, Outlet, Scripts, createRootRoute } from '@tanstack/react-router';
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'Lucide React TanStack Start integration' },
],
}),
component: RootComponent,
});
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
);
}
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
);
}
@@ -0,0 +1,15 @@
import { Link, createFileRoute } from '@tanstack/react-router';
import { ArrowLeft } from 'lucide-react';
export const Route = createFileRoute('/about')({
component: About,
});
function About() {
return (
<main>
<ArrowLeft data-testid="route-icon" />
<Link to="/">Home</Link>
</main>
);
}
@@ -0,0 +1,15 @@
import { Link, createFileRoute } from '@tanstack/react-router';
import IconShowcase from '../IconShowcase';
export const Route = createFileRoute('/')({
component: Home,
});
function Home() {
return (
<>
<IconShowcase />
<Link to="/about">About</Link>
</>
);
}
@@ -0,0 +1,38 @@
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-react';
import IconShowcase from '../src/IconShowcase';
test('renders Lucide public APIs in a real browser', async () => {
const screen = await render(<IconShowcase />);
const staticIcon = screen.getByTestId('static-icon');
await expect.element(staticIcon).toBeVisible();
await expect.element(staticIcon).toHaveAttribute('width', '24');
await expect.element(staticIcon).toHaveAttribute('height', '24');
await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24');
await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true');
const customIcon = screen.getByTestId('custom-icon');
await expect.element(customIcon).toBeVisible();
await expect.element(customIcon).toHaveAttribute('width', '48');
await expect.element(customIcon).toHaveAttribute('height', '48');
await expect.element(customIcon).toHaveAttribute('stroke', 'red');
await expect.element(customIcon).toHaveAttribute('stroke-width', '2');
await expect.element(customIcon).toHaveClass('consumer-icon');
const providerIcon = screen.getByTestId('provider-icon');
await expect.element(providerIcon).toHaveAttribute('width', '32');
await expect.element(providerIcon).toHaveAttribute('stroke', 'purple');
await expect.element(providerIcon).toHaveAttribute('stroke-width', '3');
const aliasIcon = screen.getByTestId('alias-icon');
const canonicalIcon = screen.getByTestId('canonical-icon');
await expect.element(aliasIcon).toBeVisible();
await expect.element(canonicalIcon).toBeVisible();
expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML);
const dynamicIcon = screen.getByTestId('dynamic-icon');
await expect.element(dynamicIcon).toBeVisible();
await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle');
await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden');
});
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"types": ["node", "vite/client", "vitest/globals"],
},
"include": ["src", "tests", "vite.config.ts", "vitest.config.ts"],
}
@@ -0,0 +1,7 @@
import react from '@vitejs/plugin-react';
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tanstackStart(), react()],
});
@@ -0,0 +1,15 @@
import react from '@vitejs/plugin-react';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
});
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Lucide React Vite integration</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
@@ -0,0 +1,28 @@
{
"name": "@lucide/integration-react-vite",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit",
"test:browser": "vitest run",
"check": "pnpm build && pnpm typecheck && pnpm test:browser"
},
"dependencies": {
"lucide-react": "workspace:*",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@types/react": "19.2.18",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/browser-playwright": "4.1.10",
"playwright": "1.58.2",
"typescript": "^6.0.3",
"vite": "^7.3.6",
"vitest": "4.1.10",
"vitest-browser-react": "2.2.0"
}
}
@@ -0,0 +1,5 @@
import IconShowcase from './IconShowcase';
export default function App() {
return <IconShowcase />;
}
@@ -0,0 +1,32 @@
import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react';
import { DynamicIcon } from 'lucide-react/dynamic';
export default function IconShowcase() {
return (
<main aria-label="Lucide integration">
<Camera data-testid="static-icon" />
<Droplet
data-testid="custom-icon"
className="consumer-icon"
color="red"
size={48}
strokeWidth={4}
absoluteStrokeWidth
/>
<LucideProvider
color="purple"
size={32}
strokeWidth={3}
>
<House data-testid="provider-icon" />
</LucideProvider>
<Pen data-testid="alias-icon" />
<Edit2 data-testid="canonical-icon" />
<DynamicIcon
aria-label="Dynamic circle"
data-testid="dynamic-icon"
name="circle"
/>
</main>
);
}
@@ -0,0 +1,9 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
@@ -0,0 +1,38 @@
import { expect, test } from 'vitest';
import { render } from 'vitest-browser-react';
import App from '../src/App';
test('renders Lucide public APIs in a real browser', async () => {
const screen = await render(<App />);
const staticIcon = screen.getByTestId('static-icon');
await expect.element(staticIcon).toBeVisible();
await expect.element(staticIcon).toHaveAttribute('width', '24');
await expect.element(staticIcon).toHaveAttribute('height', '24');
await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24');
await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true');
const customIcon = screen.getByTestId('custom-icon');
await expect.element(customIcon).toBeVisible();
await expect.element(customIcon).toHaveAttribute('width', '48');
await expect.element(customIcon).toHaveAttribute('height', '48');
await expect.element(customIcon).toHaveAttribute('stroke', 'red');
await expect.element(customIcon).toHaveAttribute('stroke-width', '2');
await expect.element(customIcon).toHaveClass('consumer-icon');
const providerIcon = screen.getByTestId('provider-icon');
await expect.element(providerIcon).toHaveAttribute('width', '32');
await expect.element(providerIcon).toHaveAttribute('stroke', 'purple');
await expect.element(providerIcon).toHaveAttribute('stroke-width', '3');
const aliasIcon = screen.getByTestId('alias-icon');
const canonicalIcon = screen.getByTestId('canonical-icon');
await expect.element(aliasIcon).toBeVisible();
await expect.element(canonicalIcon).toBeVisible();
expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML);
const dynamicIcon = screen.getByTestId('dynamic-icon');
await expect.element(dynamicIcon).toBeVisible();
await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle');
await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden');
});
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["vitest/globals"],
},
"include": ["src", "tests", "vite.config.ts"],
}
@@ -0,0 +1,15 @@
import react from '@vitejs/plugin-react';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react()],
test: {
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
});
+1
View File
@@ -5,6 +5,7 @@
"build": "pnpm -r --filter './packages/**' build",
"test": "pnpm -r --filter './packages/**' test",
"test:update": "pnpm -r --filter './packages/**' --filter !'./packages/lucide-angular' test -- -u",
"test:integrations:react": "pnpm --filter lucide-react build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-react/**' check",
"test:font": "pnpm --filter build-font test",
"lucide": "pnpm --filter lucide",
"lucide-angular": "pnpm --filter lucide-angular",
+1765 -183
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -2,6 +2,7 @@ packages:
- 'packages/*'
- 'tools/*'
- 'docs'
- 'integrations/*/*'
overrides:
cross-spawn: 7.0.5