feat(@lucide/svelte): Lucide svelte 5 package (#2753)

* Lucide svelte (#1)

* Update peerDependencies to support Svelte 5

* Bump svelte version

* Bump @testing-library/svelte version

* Remove alias in vitest.config.ts that causes tests to fail due to deprecated svelte/internal API

* Convert to svelte 5 syntax

* Bump vite & @sveltejs/vite-plugin-svelte version

* Fix error during render when children prop is missing & fix components being mounted on the server during tests

* Update test snapshots to reflect the differences in the html generated by svelte 5

* Convert class attribute to new array syntax with built-in clsx

* Convert export template to svelte 5 syntax

* Move svelte 5 to separate directory

* Update snapshots

* Update docs

* fix(icon): change variable declaration from let to const in Icon.svelte

* Lucide svelte (#1) (#2727)

* Update peerDependencies to support Svelte 5

* Bump svelte version

* Bump @testing-library/svelte version

* Remove alias in vitest.config.ts that causes tests to fail due to deprecated svelte/internal API

* Convert to svelte 5 syntax

* Bump vite & @sveltejs/vite-plugin-svelte version

* Fix error during render when children prop is missing & fix components being mounted on the server during tests

* Update test snapshots to reflect the differences in the html generated by svelte 5

* Convert class attribute to new array syntax with built-in clsx

* Convert export template to svelte 5 syntax

* Revert changes in lucide-svelte library

* Update package lock

* Update test files

* Formatting

* Update clean command

* Fix build

* Update packages

* update deps

* Fix export script

* Format code

* Revert version number change in package json

* Update workflows

---------

Co-authored-by: Aurélien Richard <56389380+aurelienrichard@users.noreply.github.com>
This commit is contained in:
Eric Fennis
2025-03-07 13:44:09 +01:00
committed by GitHub
parent df063fa378
commit aefb710e5c
30 changed files with 2773 additions and 1363 deletions

41
.github/workflows/lucide-svelte-5.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Lucide Svelte 5 checks
on:
pull_request:
paths:
- packages/svelte/**
- packages/shared/**
- tools/build-icons/**
- tools/rollup-plugins/**
- pnpm-lock.yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm --filter @lucide/svelte build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter @lucide/svelte test

View File

@@ -52,6 +52,7 @@ jobs:
'lucide-preact',
'lucide-solid',
'lucide-svelte',
'@lucide/svelte',
]
steps:
- uses: actions/checkout@v4

View File

@@ -7,22 +7,22 @@ Implementation of the lucide icon library for svelte applications.
::: code-group
```sh [pnpm]
pnpm add lucide-svelte
pnpm add @lucide/svelte
```
```sh [yarn]
yarn add lucide-svelte
yarn add @lucide/svelte
```
```sh [npm]
npm install lucide-svelte
npm install @lucide/svelte
```
```sh [bun]
bun add lucide-svelte
bun add @lucide/svelte
```
:::
> `@lucide/svelte` is only for Svelte 5, for Svelte 4 use the `lucide-svelte` package.
## How to use
@@ -36,7 +36,7 @@ Default usage:
```svelte
<script>
import { Skull } from 'lucide-svelte';
import { Skull } from '@lucide/svelte';
</script>
<Skull />
@@ -46,17 +46,17 @@ Additional props can be passed to adjust the icon:
```svelte
<script>
import { Camera } from 'lucide-svelte';
import { Camera } from '@lucide/svelte';
</script>
<Camera color="#ff3e98" />
```
For faster builds and load times, you can import icons directly from the `lucide-svelte/icons` directory:
For faster builds and load times, you can import icons directly from the `@lucide/svelte/icons` directory:
```svelte
<script>
import CircleAlert from 'lucide-svelte/icons/circle-alert';
import CircleAlert from '@lucide/svelte/icons/circle-alert';
</script>
<CircleAlert color="#ff3e98" />
@@ -77,7 +77,7 @@ To customize the appearance of an icon, you can pass custom properties as props
```svelte
<script>
import { Phone } from 'lucide-svelte';
import { Phone } from '@lucide/svelte';
</script>
<Phone fill="#333" />
@@ -91,101 +91,138 @@ The package includes type definitions for all icons. This is useful if you want
### TypeScript Example
#### Svelte 4
::: code-group
```svelte
```svelte [Svelte 5]
<script lang="ts">
import { Home, Library, Cog, type Icon } from 'lucide-svelte';
import type { ComponentType } from 'svelte';
import { Home, Library, Cog, type Icon as IconType } from '@lucide/svelte';
type MenuItem = {
name: string;
href: string;
icon: ComponentType<Icon>;
};
type MenuItem = {
name: string;
href: string;
icon: typeof IconType;
};
const menuItems: MenuItem[] = [
{
name: 'Home',
href: '/',
icon: Home
},
{
name: 'Blog',
href: '/blog',
icon: Library
},
{
name: 'Projects',
href: '/projects',
icon: Cog
}
];
const menuItems: MenuItem[] = [
{
name: 'Home',
href: '/',
icon: Home
},
{
name: 'Blog',
href: '/blog',
icon: Library
},
{
name: 'Projects',
href: '/projects',
icon: Cog
}
];
</script>
{#each menuItems as item}
<a href={item.href}>
<svelte:component this={item.icon} />
<span>{item.name}</span>
</a>
{@const Icon = item.icon}
<a href={item.href}>
<Icon />
<span>{item.name}</span>
</a>
{/each}
```
#### Svelte 5
Some changes are required since Svelte 5 [deprecates](https://svelte.dev/docs/svelte/v5-migration-guide#Components-are-no-longer-classes-Component-typing-changes) the `ComponentType` typescript type.
```svelte
```svelte [Svelte 4]
<script lang="ts">
import { Home, Library, Cog, type Icon as IconType } from 'lucide-svelte';
import { Home, Library, Cog, type Icon } from '@lucide/svelte';
import type { ComponentType } from 'svelte';
type MenuItem = {
name: string;
href: string;
icon: typeof IconType;
};
type MenuItem = {
name: string;
href: string;
icon: ComponentType<Icon>;
};
const menuItems: MenuItem[] = [
{
name: 'Home',
href: '/',
icon: Home
},
{
name: 'Blog',
href: '/blog',
icon: Library
},
{
name: 'Projects',
href: '/projects',
icon: Cog
}
];
const menuItems: MenuItem[] = [
{
name: 'Home',
href: '/',
icon: Home
},
{
name: 'Blog',
href: '/blog',
icon: Library
},
{
name: 'Projects',
href: '/projects',
icon: Cog
}
];
</script>
{#each menuItems as item}
{@const Icon = item.icon}
<a href={item.href}>
<Icon />
<span>{item.name}</span>
</a>
<a href={item.href}>
<svelte:component this={item.icon} />
<span>{item.name}</span>
</a>
{/each}
```
:::
### JSDoc Example
#### Svelte 4
::: code-group
```svelte
```svelte [Svelte 5]
<script>
import { Home, Library, Cog } from 'lucide-svelte';
import { Home, Library, Cog } from '@lucide/svelte';
/**
* @typedef {Object} MenuItem
* @property {string} name
* @property {string} href
* @property {import('svelte').ComponentType<import('lucide-svelte').Icon>} icon
* @property {typeof import('@lucide/svelte').Icon} icon
*/
/** @type {MenuItem[]} */
const menuItems = [
{
name: 'Home',
href: '/',
icon: Home
},
{
name: 'Blog',
href: '/blog',
icon: Library
},
{
name: 'Projects',
href: '/projects',
icon: Cog
}
];
</script>
{#each menuItems as item}
{@const Icon = item.icon}
<a href={item.href}>
<Icon />
<span>{item.name}</span>
</a>
{/each}
```
```svelte [Svelte 4]
<script>
import { Home, Library, Cog } from '@lucide/svelte';
/**
* @typedef {Object} MenuItem
* @property {string} name
* @property {string} href
* @property {import('svelte').ComponentType<import('@lucide/svelte').Icon>} icon
*/
/** @type {MenuItem[]} */
@@ -216,49 +253,7 @@ Some changes are required since Svelte 5 [deprecates](https://svelte.dev/docs/sv
{/each}
```
#### Svelte 5
```svelte
<script>
import { Home, Library, Cog } from 'lucide-svelte';
/**
* @typedef {Object} MenuItem
* @property {string} name
* @property {string} href
* @property {typeof import('lucide-svelte').Icon} icon
*/
/** @type {MenuItem[]} */
const menuItems = [
{
name: 'Home',
href: '/',
icon: Home
},
{
name: 'Blog',
href: '/blog',
icon: Library
},
{
name: 'Projects',
href: '/projects',
icon: Cog
}
];
</script>
{#each menuItems as item}
{@const Icon = item.icon}
<a href={item.href}>
<Icon />
<span>{item.name}</span>
</a>
{/each}
```
:::
For more details about typing the `svelte:component` directive, see the [Svelte documentation](https://svelte.dev/docs/typescript#types-componenttype).
@@ -275,7 +270,7 @@ This creates a single icon based on the iconNode passed and renders a Lucide ico
```svelte
<script>
import { Icon } from 'lucide-svelte';
import { Icon } from '@lucide/svelte';
import { burger, sausage } from '@lucide/lab';
</script>
@@ -293,29 +288,29 @@ The example below imports all ES Modules, so exercise caution when using it. Imp
### Icon Component Example
#### Svelte 4
::: code-group
```svelte
```svelte [Svelte 5]
<script>
import * as icons from 'lucide-svelte';
import * as icons from '@lucide/svelte';
let { name } = $props();
const Icon = icons[name];
</script>
<Icon {...props} />
```
```svelte [Svelte 4]
<script>
import * as icons from '@lucide/svelte';
export let name;
</script>
<svelte:component this={icons[name]} {...$$props} />
```
#### Svelte 5
```svelte
<script>
import * as icons from 'lucide-svelte';
let { name } = $props();
const Icon = icons[name];
</script>
<Icon {...props} />
```
:::
#### Using the Icon Component

View File

@@ -12,4 +12,4 @@
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
<path d="M6.376 18.91a6 6 0 0 1 11.249.003" />
<circle cx="12" cy="11" r="4" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 469 B

After

Width:  |  Height:  |  Size: 470 B

View File

@@ -1,36 +1,34 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Using Icon Component > should render icon and match snapshot 1`] = `
<div>
<svg
class="lucide-icon lucide"
fill="none"
height="48"
stroke="red"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
/>
<path
d="M6 8h12"
/>
<path
d="M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12"
/>
<path
d="M6.6 15.6A2 2 0 1 0 10 17v-5"
/>
</svg>
</div>
<svg
class="lucide-icon lucide"
fill="none"
height="48"
stroke="red"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
/>
<path
d="M6 8h12"
/>
<path
d="M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12"
/>
<path
d="M6.6 15.6A2 2 0 1 0 10 17v-5"
/>
</svg>
`;

View File

@@ -42,176 +42,168 @@ exports[`Using lucide icon components > should add a class to the element 1`] =
`;
exports[`Using lucide icon components > should adjust the size, stroke color and stroke width 1`] = `
<body>
<div>
<svg
class="lucide-icon lucide lucide-smile"
fill="none"
height="48"
stroke="red"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="4"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
/>
<path
d="M8 14s1.5 2 4 2 4-2 4-2"
/>
<line
x1="9"
x2="9.01"
y1="9"
y2="9"
/>
<line
x1="15"
x2="15.01"
y1="9"
y2="9"
/>
</svg>
</div>
</body>
`;
exports[`Using lucide icon components > should not scale the strokeWidth when absoluteStrokeWidth is set 1`] = `
<div>
<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"
data-testid="smile-icon"
class="lucide-icon lucide lucide-smile"
<svg
class="lucide-icon lucide lucide-smile"
fill="none"
height="48"
stroke="red"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="4"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
>
<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>
<circle
cx="12"
cy="12"
r="10"
/>
<path
d="M8 14s1.5 2 4 2 4-2 4-2"
/>
<line
x1="9"
x2="9.01"
y1="9"
y2="9"
/>
<line
x1="15"
x2="15.01"
y1="9"
y2="9"
/>
</svg>
</div>
`;
exports[`Using lucide icon components > should render an component 1`] = `
<body>
<div>
<svg
class="lucide-icon lucide lucide-smile"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
/>
<path
d="M8 14s1.5 2 4 2 4-2 4-2"
/>
<line
x1="9"
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"
data-testid="smile-icon"
class="lucide-icon lucide lucide-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
x1="15"
>
</line>
<line x1="15"
x2="15.01"
y1="9"
y2="9"
/>
</svg>
</div>
</body>
>
</line>
</svg>
`;
exports[`Using lucide icon components > should render an component 1`] = `
<div>
<svg
class="lucide-icon lucide lucide-smile"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
/>
<path
d="M8 14s1.5 2 4 2 4-2 4-2"
/>
<line
x1="9"
x2="9.01"
y1="9"
y2="9"
/>
<line
x1="15"
x2="15.01"
y1="9"
y2="9"
/>
</svg>
</div>
`;
exports[`Using lucide icon components > should render an icon slot 1`] = `
<body>
<div>
<svg
class="lucide-icon lucide lucide-smile"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
/>
<path
d="M8 14s1.5 2 4 2 4-2 4-2"
/>
<line
x1="9"
x2="9.01"
y1="9"
y2="9"
/>
<line
x1="15"
x2="15.01"
y1="9"
y2="9"
/>
<text>
Test
</text>
</svg>
</div>
</body>
<div>
<svg
class="lucide-icon lucide lucide-smile"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
/>
<path
d="M8 14s1.5 2 4 2 4-2 4-2"
/>
<line
x1="9"
x2="9.01"
y1="9"
y2="9"
/>
<line
x1="15"
x2="15.01"
y1="9"
y2="9"
/>
<text>
Test
</text>
</svg>
</div>
`;

2
packages/svelte/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
src/icons/*.svelte
.svelte-kit

73
packages/svelte/README.md Normal file
View File

@@ -0,0 +1,73 @@
<p align="center">
<a href="https://github.com/lucide-icons/lucide">
<img src="https://lucide.dev/package-logos/lucide-svelte.svg" alt="Lucide icon library for Svelte 5 applications." width="540">
</a>
</p>
<p align="center">
Lucide icon library for Svelte 5 applications.
</p>
<div align="center">
[![npm](https://img.shields.io/npm/v/%40lucide%2Fsvelte?color=blue)](https://www.npmjs.com/package/lucide-svelte)
![NPM Downloads](https://img.shields.io/npm/dw/%40lucide%2Fsvelte)
[![GitHub](https://img.shields.io/github/license/lucide-icons/lucide)](https://lucide.dev/license)
</div>
<p align="center">
<a href="https://lucide.dev/guide/">About</a>
·
<a href="https://lucide.dev/icons/">Icons</a>
·
<a href="https://lucide.dev/guide/packages/lucide-svelte">Documentation</a>
·
<a href="https://lucide.dev/license">License</a>
</p>
# Lucide Svelte
Implementation of the lucide icon library for svelte applications.
## Installation
```sh
pnpm add @lucide/svelte
```
```sh
npm install @lucide/svelte
```
```sh
yarn add @lucide/svelte
```
```sh
bun add @lucide/svelte
```
## Documentation
For full documentation, visit [lucide.dev](https://lucide.dev/guide/packages/lucide-svelte)
## 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
<a href="https://vercel.com?utm_source=lucide&utm_campaign=oss">
<img src="https://lucide.dev/vercel.svg" alt="Powered by Vercel" width="200" />
</a>
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
### Awesome backers 🍺
<a href="https://www.scipress.io?utm_source=lucide"><img src="https://lucide.dev/sponsors/scipress.svg" width="180" alt="Scipress sponsor badge" /></a>
<a href="https://github.com/pdfme/pdfme"><img src="https://lucide.dev/sponsors/pdfme.svg" width="180" alt="pdfme sponsor badge" /></a>

View File

@@ -0,0 +1,79 @@
{
"name": "@lucide/svelte",
"description": "A Lucide icon library package for Svelte applications",
"version": "0.1.0",
"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/svelte"
},
"keywords": [
"Lucide",
"Svelte",
"Feather",
"Icons",
"Icon",
"SVG",
"Feather Icons",
"Fontawesome",
"Font Awesome"
],
"author": "Eric Fennis",
"type": "module",
"main": "dist/lucide-svelte.js",
"exports": {
".": {
"types": "./dist/lucide-svelte.d.ts",
"svelte": "./dist/lucide-svelte.js",
"default": "./dist/lucide-svelte.js"
},
"./icons": {
"types": "./dist/lucide-svelte.d.ts",
"svelte": "./dist/lucide-svelte.js"
},
"./icons/*": {
"types": "./dist/icons/*.svelte.d.ts",
"svelte": "./dist/icons/*.js",
"default": "./dist/icons/*.js"
}
},
"typings": "dist/lucide-svelte.d.ts",
"sideEffects": false,
"files": [
"dist"
],
"scripts": {
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm build:package && pnpm build:license",
"copy:license": "cp ../../LICENSE ./LICENSE",
"clean": "rm -rf dist stats ./src/icons/*.{ts,svelte} ./src/aliases/{aliases,prefixed,suffixed}.ts",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mjs --exportFileName=index.ts --iconFileExtension=.svelte --importImportFileExtension=.svelte --separateIconFileExport --separateIconFileExportExtension=.ts --withAliases --aliasesFileExtension=.ts --separateAliasesFile --separateAliasesFileExtension=.ts --aliasImportFileExtension=.js --pretty=false",
"build:package": "svelte-package --input ./src",
"build:license": "node ./scripts/appendBlockComments.mjs",
"test": "pnpm copy:license && pnpm build:icons && vitest run",
"test:watch": "vitest watch",
"version": "pnpm version --git-tag-version=false"
},
"devDependencies": {
"@lucide/build-icons": "workspace:*",
"@lucide/helpers": "workspace:*",
"@sveltejs/package": "^2.3.10",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/svelte": "^5.2.7",
"@tsconfig/svelte": "^5.0.4",
"jest-serializer-html": "^7.1.0",
"jsdom": "^20.0.3",
"svelte": "^5.20.5",
"svelte-check": "^4.1.4",
"svelte-preprocess": "^6.0.3",
"typescript": "^5.1.6",
"vite": "6.0.7",
"vitest": "^1.1.1"
},
"peerDependencies": {
"svelte": "^5"
}
}

View File

@@ -0,0 +1,64 @@
import { lstatSync } from 'fs';
import { readdir, readFile, writeFile } from 'fs/promises';
import path from 'path';
import { getCurrentDirPath } from '@lucide/helpers';
import { getJSBanner } from './license.mjs';
const currentDir = getCurrentDirPath(import.meta.url);
const targetDirectory = path.join(currentDir, '../dist');
const files = await readdir(targetDirectory, {
recursive: true,
encoding: 'utf-8',
});
// eslint-disable-next-line no-restricted-syntax
for (const file of files) {
const filepath = path.join(targetDirectory, file);
const filestat = lstatSync(filepath);
// eslint-disable-next-line no-continue
if (filestat.isFile() === false || filestat.isDirectory()) continue;
// eslint-disable-next-line no-await-in-loop
const contents = await readFile(filepath, { encoding: 'utf-8' });
let newContents = contents;
const ext = path.extname(filepath);
let license;
if (/\.(js|mjs|cjs|ts)/.test(ext)) {
license = getJSBanner();
}
if (license) {
newContents = license + contents;
}
// Places icon block comment at the top of the Svelte component class
if (/icons\/(.*?)\.svelte\.d\.ts/.test(filepath)) {
const svelteFilepath = filepath.replace('.d.ts', '');
// eslint-disable-next-line no-await-in-loop
const svelteFileContents = await readFile(svelteFilepath, { encoding: 'utf-8' });
const blockCommentRegex = /\/\*\*\n\s\*\s(@component\s@name)[\s\S]*?\*\//;
const blockCommentMatch = blockCommentRegex.exec(svelteFileContents);
if (blockCommentMatch !== null) {
const blockComment = blockCommentMatch[0];
const exportClassRegex = /export default class (\w+) extends SvelteComponentTyped<(.*?)> {/;
if (exportClassRegex.test(newContents)) {
newContents = newContents.replace(
exportClassRegex,
`${blockComment}\nexport default class $1 extends SvelteComponentTyped<$2> {`,
);
}
}
}
if (newContents !== contents) {
// eslint-disable-next-line no-await-in-loop
await writeFile(filepath, newContents, { encoding: 'utf-8' });
}
}

View File

@@ -0,0 +1,43 @@
/* eslint-disable import/no-extraneous-dependencies */
import base64SVG from '@lucide/build-icons/utils/base64SVG.mjs';
import { getJSBanner } from './license.mjs';
export default async ({
iconName,
children,
componentName,
getSvg,
deprecated,
deprecationReason,
}) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
return `\
<script lang="ts">
${getJSBanner()}
import Icon from '../Icon.svelte';
import type { IconNode, IconProps } from '../types.js';
let props: IconProps = $props();
const iconNode: IconNode = ${JSON.stringify(children)};
/**
* @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-svelte - Documentation
*
* @param {Object} props - Lucide icons props and any valid SVG attribute
* @returns {FunctionalComponent} Svelte component
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
</script>
<Icon name="${iconName}" {...props} iconNode={iconNode}>
{@render props.children?.()}
</Icon>
`;
};

View File

@@ -0,0 +1,13 @@
import fs from 'fs';
import pkg from '../package.json' with { type: 'json' };
const license = fs.readFileSync('LICENSE', 'utf-8');
export function getJSBanner() {
return `/**
* @license ${pkg.name} v${pkg.version} - ${pkg.license}
*
* ${license.split('\n').join('\n * ')}
*/
`;
}

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import defaultAttributes from './defaultAttributes';
import type { IconProps } from './types';
const {
name,
color = 'currentColor',
size = 24,
strokeWidth = 2,
absoluteStrokeWidth = false,
iconNode = [],
children,
...props
}: IconProps = $props();
</script>
<svg
{...defaultAttributes}
{...props}
width={size}
height={size}
stroke={color}
stroke-width={absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth}
class={['lucide-icon lucide', name && `lucide-${name}`, props.class]}
>
{#each iconNode as [tag, attrs]}
<svelte:element
this={tag}
{...attrs}
/>
{/each}
{@render children?.()}
</svg>

View File

@@ -0,0 +1,3 @@
export * from './aliases';
export * from './prefixed';
export * from './suffixed';

View File

@@ -0,0 +1,15 @@
import type { Attrs } from './types.js';
const defaultAttributes: Attrs = {
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',
};
export default defaultAttributes;

View File

@@ -0,0 +1 @@
Folder for generated icons

View File

@@ -0,0 +1,6 @@
export * from './icons/index';
export * as icons from './icons/index';
export * from './aliases';
export { default as defaultAttributes } from './defaultAttributes';
export * from './types';
export { default as Icon } from './Icon.svelte';

View File

@@ -0,0 +1,24 @@
import type { SVGAttributes, SvelteHTMLElements } from 'svelte/elements';
import type { Snippet } from 'svelte';
export type Attrs = SVGAttributes<SVGSVGElement>;
export type IconNode = [elementName: keyof SvelteHTMLElements, attrs: Attrs][];
export interface IconProps extends Attrs {
name?: string;
color?: string;
size?: number | string;
strokeWidth?: number | string;
absoluteStrokeWidth?: boolean;
iconNode?: IconNode;
children?: Snippet;
}
export type IconEvents = {
[evt: string]: CustomEvent<any>;
};
export type IconSlots = {
default: {};
};

View File

@@ -0,0 +1,8 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import sveltePreprocess from 'svelte-preprocess';
export default {
preprocess: sveltePreprocess({
typescript: true,
}),
};

View File

@@ -0,0 +1,33 @@
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/svelte';
import { Icon } from '../src/lucide-svelte';
import { airVent } from './testIconNodes';
describe('Using Icon Component', () => {
it('should render icon based on a iconNode', async () => {
const { container } = render(Icon, {
props: {
iconNode: airVent,
size: 48,
color: 'red',
absoluteStrokeWidth: true,
},
});
expect(container.firstChild).toBeDefined();
});
it('should render icon and match snapshot', async () => {
const { container } = render(Icon, {
props: {
iconNode: airVent,
size: 48,
color: 'red',
absoluteStrokeWidth: true,
},
});
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import Smile from '../src/icons/smile.svelte'
</script>
<Smile>
<text>Test</text>
</Smile>

View File

@@ -0,0 +1,47 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Using Icon Component > should render icon and match snapshot 1`] = `
<svg
class="lucide-icon lucide"
fill="none"
height="48"
stroke="red"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
>
<!---->
<path
d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
>
</path>
<!---->
<path
d="M6 8h12"
>
</path>
<!---->
<path
d="M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12"
>
</path>
<!---->
<path
d="M6.6 15.6A2 2 0 1 0 10 17v-5"
>
</path>
<!---->
<!---->
</svg>
`;

View File

@@ -0,0 +1,272 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Using lucide icon components > should add a class to the element 1`] = `
<svg
class="lucide-icon lucide lucide-smile my-icon"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<!---->
<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>
`;
exports[`Using lucide icon components > should adjust the size, stroke color and stroke width 1`] = `
<div>
<svg
class="lucide-icon lucide lucide-smile"
fill="none"
height="48"
stroke="red"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="4"
viewBox="0 0 24 24"
width="48"
xmlns="http://www.w3.org/2000/svg"
>
<!---->
<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>
</div>
`;
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"
data-testid="smile-icon"
class="lucide-icon lucide lucide-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>
`;
exports[`Using lucide icon components > should render an component 1`] = `
<div>
<svg
class="lucide-icon lucide lucide-smile"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<!---->
<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>
</div>
`;
exports[`Using lucide icon components > should render an icon slot 1`] = `
<div>
<svg
class="lucide-icon lucide lucide-smile"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<!---->
<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>
<!---->
<!---->
<text>
Test
</text>
<!---->
</svg>
</div>
`;

View File

@@ -0,0 +1,92 @@
import { describe, it, expect, afterEach } from 'vitest';
import { render, cleanup } from '@testing-library/svelte';
import { Smile, Pen, Edit2 } from '../src/lucide-svelte';
import TestSlots from './TestSlots.svelte';
describe('Using lucide icon components', () => {
afterEach(() => cleanup());
it('should render an component', () => {
const { container } = render(Smile);
expect(container).toMatchSnapshot();
});
it('should adjust the size, stroke color and stroke width', () => {
const { container } = render(Smile, {
props: {
size: 48,
color: 'red',
strokeWidth: 4,
},
});
expect(container).toMatchSnapshot();
});
it('should add a class to the element', () => {
const testClass = 'my-icon';
render(Smile, {
props: {
class: testClass,
},
});
const [icon] = document.getElementsByClassName(testClass);
expect(icon).toBeInTheDocument();
expect(icon).toMatchSnapshot();
expect(icon).toHaveClass(testClass);
expect(icon).toHaveClass('lucide');
expect(icon).toHaveClass('lucide-smile');
});
it('should add a style attribute to the element', () => {
render(Smile, {
props: {
style: 'position: absolute;',
},
});
const [icon] = document.getElementsByClassName('lucide');
expect(icon.getAttribute('style')).toContain('position: absolute');
});
it('should render an icon slot', () => {
const { container, getByText } = render(TestSlots);
const textElement = getByText('Test');
expect(textElement).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render the alias icon', () => {
const { container } = render(Pen);
const PenIconRenderedHTML = container.innerHTML;
cleanup();
const { container: Edit2Container } = render(Edit2);
expect(PenIconRenderedHTML).toBe(Edit2Container.innerHTML);
});
it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => {
const testId = 'smile-icon';
const { container, getByTestId } = render(Smile, {
'data-testid': testId,
color: 'red',
size: 48,
absoluteStrokeWidth: true,
});
const { attributes } = getByTestId(testId) as unknown as {
attributes: Record<string, { value: string }>;
};
expect(attributes.stroke.value).toBe('red');
expect(attributes.width.value).toBe('48');
expect(attributes.height.value).toBe('48');
expect(attributes['stroke-width'].value).toBe('1');
expect(container.innerHTML).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,5 @@
import { expect } from 'vitest';
import '@testing-library/jest-dom/vitest';
import htmlSerializer from 'jest-serializer-html';
expect.addSnapshotSerializer(htmlSerializer);

View File

@@ -0,0 +1,21 @@
import type { IconNode } from '../src/lucide-svelte';
export const airVent: IconNode = [
[
'path',
{
d: 'M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2',
},
],
['path', { d: 'M6 8h12' }],
['path', { d: 'M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12' }],
['path', { d: 'M6.6 15.6A2 2 0 1 0 10 17v-5' }],
];
export const coffee: IconNode = [
['path', { d: 'M17 8h1a4 4 0 1 1 0 8h-1' }],
['path', { d: 'M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z' }],
['line', { x1: '6', x2: '6', y1: '2', y2: '4' }],
['line', { x1: '10', x2: '10', y1: '2', y2: '4' }],
['line', { x1: '14', x2: '14', y1: '2', y2: '4' }],
];

View File

@@ -0,0 +1,14 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
"allowJs": true,
"checkJs": true,
"isolatedModules": true,
"types": ["@testing-library/jest-dom"],
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "tests/**/*.ts"],
}

View File

@@ -0,0 +1,19 @@
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vitest/config';
// @ts-expect-error - type mismatch
export default defineConfig(({ mode }) => ({
plugins: [
svelte({
compilerOptions: { hmr: false },
}),
],
resolve: {
conditions: mode === 'test' ? ['browser'] : [],
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setupVitest.ts',
},
}));

2581
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ function pascalToKebabNextJSFlavour(str) {
const currentDir = getCurrentDirPath(import.meta.url);
const ICONS_DIR = path.resolve(currentDir, '../icons');
const svgFiles = readSvgDirectory(ICONS_DIR);
const svgFiles = await readSvgDirectory(ICONS_DIR);
const iconNames = svgFiles.map((icon) => icon.split('.')[0]).reverse();