chore(dev): upgrade ESLint to latest compatible stack (v10) (#4378)

* chore: upgrade eslint to v9 latest with compatible lint deps

Agent-Logs-Url: https://github.com/lucide-icons/lucide/sessions/f7c1f1b5-d213-4afa-91a8-b799a16397ec

* Fix eslint config

* Fix a lot of eslint errors

* Fix eslint

* Fix eslint errors

* Fix eslint errors

* Fix types

* Fix eslint warning

* Format code

* Add eslint 10

* Remove unused vars

* Update pnpm

* Update deps

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Eric Fennis <eric.fennis@gmail.com>
This commit is contained in:
Copilot
2026-08-18 13:09:22 +02:00
committed by GitHub
parent 7367c8f0fc
commit 75b55160aa
78 changed files with 1001 additions and 1231 deletions

View File

@@ -1,11 +0,0 @@
dist
build
coverage
lib
tests
node_modules
.eslintrc.js
docs/images
docs/**/examples/
packages/lucide-react/dynamicIconImports.js
packages/angular/.angular

View File

@@ -1,75 +0,0 @@
const DEFAULT_ATTRS = require('./tools/build-icons/render/default-attrs.json');
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
extends: ['airbnb-base', 'prettier'],
plugins: ['import', '@html-eslint'],
rules: {
'no-console': 'off',
'no-param-reassign': 'off',
'no-shadow': 'off',
'no-use-before-define': 'off',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: ['**/*.test.js', '**/*.spec.js', '**/scripts/**'],
},
],
'import/extensions': [
'error',
{
pattern: {
mjs: 'always',
json: 'always',
},
},
],
},
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./docs/tsconfig.json', './packages/*/tsconfig.json'],
ecmaVersion: 'latest',
sourceType: 'module',
},
overrides: [
{
files: ['./icons/*.svg'],
parser: '@html-eslint/parser',
rules: {
'@html-eslint/require-doctype': 'off',
'@html-eslint/no-duplicate-attrs': 'error',
'@html-eslint/no-inline-styles': 'error',
'@html-eslint/require-attrs': [
'error',
...Object.entries(DEFAULT_ATTRS).map(([attr, value]) => ({
tag: 'svg',
attr,
value: String(value),
})),
],
'@html-eslint/indent': ['error', 2],
'@html-eslint/no-multiple-empty-lines': ['error', { max: 0 }],
'@html-eslint/no-extra-spacing-attrs': [
'error',
{
enforceBeforeSelfClose: true,
},
],
'@html-eslint/require-closing-tags': [
'error',
{
selfClosing: 'always',
allowSelfClosingCustom: true,
},
],
'@html-eslint/element-newline': 'error',
'@html-eslint/no-trailing-spaces': 'error',
'@html-eslint/quotes': 'error',
},
},
],
};

View File

@@ -78,8 +78,10 @@ updates:
- 'vite-plugin-*'
linting-deps:
patterns:
- '@eslint/*'
- '@html-eslint/*'
- '@typescript-eslint/*'
- 'angular-eslint'
- 'cspell'
- 'eslint'
- 'eslint-*'

View File

@@ -1,5 +1,5 @@
{
"cSpell.words": ["devs", "preact", "Preact"],
"cSpell.words": ["devs", "linecap", "linejoin", "preact", "Preact"],
"eslint.enable": true,
"eslint.validate": ["javascript", "svg"],
"svg.preview.background": "editor"

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
interface BackdropProps {
src: string;

View File

@@ -3,7 +3,6 @@ import pathToPoints from './path-to-points';
import { Path, PathProps } from './types';
export const GapViolationHighlight = ({
radius,
stroke,
strokeWidth,
strokeOpacity,

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import { PathProps, Path } from './types';
import getPaths, { assert } from './utils';
import { GapViolationHighlight } from './GapViolationHighlight.tsx';
@@ -293,7 +293,7 @@ const Radii = ({
...props
}: { paths: Path[] } & PathProps<
'strokeWidth' | 'stroke' | 'strokeDasharray' | 'strokeOpacity',
any
never
>) => {
return (
<g
@@ -361,12 +361,12 @@ const Radii = ({
const Handles = ({
paths,
...props
}: { paths: Path[] } & PathProps<'strokeWidth' | 'stroke' | 'strokeOpacity', any>) => (
}: { paths: Path[] } & PathProps<'strokeWidth' | 'stroke' | 'strokeOpacity', never>) => (
<g
className="svg-preview-handles-group"
{...props}
>
{paths.map(({ c, prev, next, cp1, cp2 }, i) => (
{paths.map(({ prev, next, cp1, cp2 }, i) => (
<React.Fragment key={i}>
{cp1 && <path d={`M${prev.x} ${prev.y} ${cp1.x} ${cp1.y}`} />}
{cp1 && (

View File

@@ -12,7 +12,9 @@ function pathToPoints({ d, prev, next }: Path, interval = 1) {
points.push(commander.getPointAtLength(i));
}
points.push(next);
} catch (err) {}
} catch (err) {
console.warn(`Failed to convert path to points for d="${d}": ${err}`);
}
return points;
}

View File

@@ -55,7 +55,7 @@ export const getCommands = (src: string) =>
.flatMap(({ d, name }, idx) =>
new SVGPathData(d)
.toAbs()
// @ts-ignore
// @ts-expect-error `commands` is typed as a union of command types svgson does not narrow
.commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })),
);
@@ -242,7 +242,7 @@ const getPaths = (src: string) => {
break;
}
default: {
// @ts-ignore
// @ts-expect-error every command type is handled above, so `c` is not `never` here
assertNever(c);
}
}

View File

@@ -9,11 +9,9 @@ export const hash = (string: string, seed = 5381) => {
let i = string.length;
while (i) {
// eslint-disable-next-line no-bitwise, no-plusplus
seed = (seed * 33) ^ string.charCodeAt(--i);
}
// eslint-disable-next-line no-bitwise
return (seed >>> 0).toString(36).substr(0, 6);
};

View File

@@ -1,5 +1,6 @@
import type MarkdownIt from 'markdown-it';
import type { RenderRule } from 'markdown-it/lib/renderer.mjs';
import type Token from 'markdown-it/lib/token.mjs';
import container from 'markdown-it-container';
import sandpackTheme from '../theme/sandpackTheme.json';
@@ -21,14 +22,10 @@ export default function sandpackPlugin(md: MarkdownIt, pluginOptions: SnackParam
throw new Error('MarkdownIt instance is required for sandpackPlugin');
}
const escapeHtml = md?.utils?.escapeHtml;
const defaultFence =
md.renderer.rules.fence ||
((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));
const renderSandbox = (tokenList: any[], index: number) => {
const renderFunc = (tokens: any[], idx: number) => {
const renderSandbox = (tokenList: Token[], index: number) => {
const renderFunc = (tokens: Token[], idx: number) => {
if (tokens[idx].nesting === 1) {
const fileAttr: string[] = [];
const attrs = Object.fromEntries(tokens[idx].attrs || []);
const files: Record<
@@ -47,7 +44,7 @@ export default function sandpackPlugin(md: MarkdownIt, pluginOptions: SnackParam
) {
if (tokens[i].type === 'fence' && tokens[i].tag === 'code') {
const info = tokens[i].info ?? '';
const [lang, fileName, params = ''] = info.split(' ');
const [, fileName, params = ''] = info.split(' ');
const active = params.includes('[active]');
const hidden = params.includes('[hidden]');
@@ -57,15 +54,9 @@ export default function sandpackPlugin(md: MarkdownIt, pluginOptions: SnackParam
if (fileName && code) {
files[fileName] = {
code,
...(active && { active: true }),
...(hidden && { hidden: true }),
};
if (active) {
(files[fileName] as any).active = true;
}
if (hidden) {
(files[fileName] as any).hidden = true;
}
}
}
}
@@ -122,7 +113,7 @@ export default function sandpackPlugin(md: MarkdownIt, pluginOptions: SnackParam
return renderFunc(tokenList, index);
};
function createCodeGroup(md: MarkdownIt): ContainerArgs {
function createCodeGroup(): ContainerArgs {
return [
container,
'sandpack',
@@ -134,5 +125,5 @@ export default function sandpackPlugin(md: MarkdownIt, pluginOptions: SnackParam
];
}
md.use(...createCodeGroup(md));
md.use(...createCodeGroup());
}

View File

@@ -73,7 +73,6 @@ export default function snackPlayerPlugin(md: MarkdownIt) {
'react-native-safe-area-context' + (params.dependencies ? `,${params.dependencies}` : '');
const platform = params.platform ?? 'web';
const supportedPlatforms = params.supportedPlatforms ?? 'ios,android,web';
const theme = params.theme ?? 'light';
const preview = params.preview ?? 'true';
const loading = params.loading ?? 'lazy';
const deviceAppearance = params.deviceAppearance ?? 'dark';
@@ -88,7 +87,7 @@ export default function snackPlayerPlugin(md: MarkdownIt) {
` data-snack-dependencies="${escapeHtml(dependencies)}"` +
` data-snack-platform="${escapeHtml(platform)}"` +
` data-snack-supported-platforms="${escapeHtml(supportedPlatforms)}"` +
// ` data-snack-theme="${escapeHtml(theme)}"` +
// ` data-snack-theme="${escapeHtml(params.theme ?? 'light')}"` +
` data-snack-preview="${escapeHtml(preview)}"` +
` data-snack-loading="${escapeHtml(loading)}"` +
` data-snack-device-appearance="${escapeHtml(deviceAppearance)}"` +

View File

@@ -1,4 +1,3 @@
import { getAllData } from '../../../lib/icons';
import { getAllCategoryFiles, mapCategoryIconCount } from '../../../lib/categories';
import iconsMetaData from '../../../data/iconMetaData';

View File

@@ -1,5 +1,3 @@
/* eslint-disable no-console */
import { ref, inject, Ref } from 'vue';
export const ICON_STYLE_CONTEXT = Symbol('style');

View File

@@ -6,13 +6,13 @@ declare module '*.vue' {
}
declare module '*.data.ts' {
const data: any;
const data: unknown;
export { data };
}
declare module '*.data' {
const data: any;
const data: unknown;
export { data };
}
@@ -75,7 +75,7 @@ declare global {
doNotShowAfterSubmit?: boolean;
customFormUrl?: string; // when you want to load the form via it's custom domain URL
hiddenFields?: {
[key: string]: any;
[key: string]: unknown;
};
onOpen?: () => void;
onClose?: () => void;

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import fs from 'fs';
import path from 'path';
import { simpleGit } from 'simple-git';

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import fs from 'fs';
import path from 'path';
import { loadEnvFile } from 'node:process';
@@ -10,7 +9,9 @@ const dataDirectory = path.resolve(currentDir, '.vitepress/data');
try {
// Load environment variables from .env file, if it exists.
loadEnvFile(`${currentDir}/.env`);
} catch (error) {}
} catch {
// No .env file, so rely on the environment as-is.
}
interface IconCollection {
/** Human readable name, used for logging. */

154
eslint.config.js Normal file
View File

@@ -0,0 +1,154 @@
import path from 'node:path';
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import { defineConfig } from 'eslint/config';
import prettier from 'eslint-config-prettier';
import importX, { createNodeResolver } from 'eslint-plugin-import-x';
import htmlEslint from '@html-eslint/eslint-plugin';
import htmlParser from '@html-eslint/parser';
import defaultAttrs from './tools/build-icons/render/default-attrs.json' with { type: 'json' };
import tseslint from 'typescript-eslint';
const gitignorePath = path.join(import.meta.dirname, '.gitignore');
export default defineConfig([
tseslint.configs.recommended,
{
// `packages/angular` has its own config with its own tsconfig, so a single `eslint .` run sees
// two candidate roots and typescript-eslint refuses to guess between them.
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
},
},
},
// Everything git ignores (generated icon sources, build output, caches) is ignored here too.
includeIgnoreFile(gitignorePath),
{
// Ignores that are not in .gitignore, because these files are committed.
ignores: [
'lib',
'**/tests',
'packages/**/tests/*',
'docs/images',
'docs/**/examples/',
'docs/.vitepress/theme/components/editors/preact/index.js',
'packages/svelte/.svelte-kit',
// Tracked in git despite matching a .gitignore pattern, so lint it.
'!packages/lucide-react/dynamicIconImports.mjs',
],
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
console: 'readonly',
process: 'readonly',
global: 'readonly',
__dirname: 'readonly',
},
},
plugins: {
'import-x': importX,
},
settings: {
'import-x/resolver-next': [createNodeResolver()],
},
rules: {
...js.configs.recommended.rules,
...prettier.rules,
'no-console': 'off',
'no-param-reassign': 'off',
'no-shadow': 'off',
'no-use-before-define': 'off',
'import-x/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'**/*.test.js',
'**/*.spec.js',
'**/scripts/**',
'eslint.config.js',
'packages/**/tests/**',
],
},
],
'import-x/extensions': [
'error',
{
pattern: {
mjs: 'always',
json: 'always',
},
},
],
},
},
{
rules: {
// Omitting a property by destructuring it away (`const { key, ...attrs } = node`) leaves the
// omitted binding unused on purpose, so don't report it.
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
},
},
{
files: ['./icons/*.svg'],
languageOptions: {
parser: htmlParser,
},
plugins: {
'@html-eslint': htmlEslint,
},
rules: {
'@html-eslint/require-doctype': 'off',
'@html-eslint/no-duplicate-attrs': 'error',
'@html-eslint/no-inline-styles': 'error',
'@html-eslint/require-attrs': [
'error',
...Object.entries(defaultAttrs).map(([attr, value]) => ({
tag: 'svg',
attr,
value: String(value),
})),
],
'@html-eslint/indent': ['error', 2],
'@html-eslint/no-multiple-empty-lines': ['error', { max: 0 }],
'@html-eslint/no-extra-spacing-attrs': [
'error',
{
enforceBeforeSelfClose: true,
},
],
'@html-eslint/attrs-newline': [
'error',
{
inline: ['path', 'line', 'polyline', 'polygon', 'rect', 'circle', 'ellipse'],
},
],
'@html-eslint/require-closing-tags': [
'error',
{
selfClosing: 'always',
// Inside <svg> every tag counts as "foreign", where the rule only checks that a tag
// already written `/>` stays that way. Listing the shapes here is what actually forces
// `<path ...></path>` to become `<path ... />`.
selfClosingCustomPatterns: ['^(path|line|polyline|polygon|rect|circle|ellipse)$'],
},
],
'@html-eslint/no-restricted-attr-values': [
'error',
{
attrPatterns: ['^(fill|stroke)$'],
attrValuePatterns: ['^(?!(none|currentColor)$).*$'],
message:
'Icons must inherit their colors: `fill` and `stroke` may only be `none` or `currentColor`.',
},
],
'@html-eslint/element-newline': 'error',
'@html-eslint/no-trailing-spaces': 'error',
'@html-eslint/quotes': 'error',
},
},
]);

View File

@@ -1,5 +1,6 @@
{
"private": true,
"type": "module",
"scripts": {
"build": "pnpm -r --filter './packages/**' build",
"test": "pnpm -r --filter './packages/**' test",
@@ -22,7 +23,6 @@
"addjsons": "node ./scripts/addMissingIconJsonFiles.mts",
"checkIcons": "node ./scripts/checkIconsAndCategories.mts",
"checkLabIcons": "node ./scripts/checkLabIcons.mts",
"generate:changelog": "node ./scripts/generateChangelog.mts",
"generate:sponsors": "node scripts/updateSponsors.mts",
"generate:contributors": "node ./scripts/updateContributors.mts icons/*.svg",
"generate:nextJSAliases": "node ./scripts/generateNextJSAliases.mts",
@@ -47,25 +47,22 @@
"devDependencies": {
"@actions/core": "^3.0.1",
"@actions/github": "^9.1.1",
"@html-eslint/eslint-plugin": "^0.19.1",
"@html-eslint/parser": "^0.19.1",
"@eslint/compat": "^2.1.0",
"@eslint/js": "^10.0.1",
"@html-eslint/eslint-plugin": "^0.60.0",
"@html-eslint/parser": "^0.60.0",
"@octokit/rest": "^19.0.13",
"@types/yargs": "^17.0.35",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@typescript-eslint/eslint-plugin": "^8.59.3",
"@typescript-eslint/parser": "^8.59.3",
"@vitest/coverage-v8": "4.1.10",
"@vitest/ui": "4.1.10",
"ajv-cli": "^5.0.0",
"cspell": "^10.0.1",
"dotenv": "^17.4.2",
"eslint": "^8.57.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^8.10.2",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-custom-alias": "^1.3.2",
"eslint-import-resolver-typescript": "^3.10.1",
"eslint-plugin-import": "^2.32.0",
"eslint": "^10.8.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import-x": "^4.17.1",
"husky": "^8.0.3",
"jsdom": "^27.4.0",
"lint-staged": "^17.0.8",
@@ -78,6 +75,8 @@
"simple-git": "^3.36.0",
"svgo": "^3.3.4",
"svgson": "^5.3.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.4",
"vitest": "4.1.10",
"yargs": "^17.7.3",
"zod": "^3.25.76"
@@ -85,5 +84,5 @@
"engines": {
"node": ">=24.11.1"
},
"packageManager": "pnpm@11.16.0+sha512.b767e9a98fc87aec0f42daefcd0e84941c2cdb45d73c4e1e68860fb2bdfdbe032ab592105479e5e05c237f740c8a5ffdbbb52385797ddc2296745a1bbb883e8d"
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,38 +0,0 @@
module.exports = {
root: true,
overrides: [
{
files: ['*.ts'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@angular-eslint/recommended',
'plugin:@angular-eslint/template/process-inline-templates',
'prettier',
],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'lucide',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'attribute',
prefix: ['lucide'],
style: 'camelCase',
},
],
},
},
{
files: ['*.html'],
extends: ['plugin:@angular-eslint/template/recommended'],
rules: {},
},
],
};

View File

@@ -0,0 +1,68 @@
import tseslint from 'typescript-eslint';
import angular from 'angular-eslint';
import prettier from 'eslint-config-prettier';
import { defineConfig } from 'eslint/config';
export default defineConfig([
{
// ESLint resolves a config per linted file by walking up from that file, so this config — not
// the workspace root one — owns everything under `packages/angular`. That means the root's
// `.gitignore`-derived ignores do not apply here and generated output must be listed by hand.
ignores: [
'dist',
'build',
'coverage',
'out-tsc',
'.angular',
'src/icons', // Generated by `pnpm build:icons`.
'eslint.config.mjs',
],
},
{
files: ['**/*.ts'],
extends: [tseslint.configs.recommended, angular.configs.tsRecommended],
languageOptions: {
parserOptions: {
// `tsconfig.json` here is solution-style (`files: []` plus `references`), so `project: true`
// resolves to a project that contains no source file. The project service follows the
// references the way `tsc` and the editor do.
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
...prettier.rules,
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'lucide',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'attribute',
prefix: ['lucide'],
style: 'camelCase',
},
],
},
},
{
// Build scripts belong to no tsconfig, so they get the untyped ruleset.
files: ['**/*.mts', '**/*.cts'],
extends: [tseslint.configs.recommended],
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
},
},
rules: prettier.rules,
},
{
files: ['**/*.html'],
extends: [angular.configs.templateRecommended],
},
]);

View File

@@ -25,7 +25,7 @@
"build:ng": "ng build --configuration production",
"test": "pnpm build:icons && ng test --no-watch",
"test:watch": "ng test",
"lint": "npx eslint 'src/**/*.{js,jsx,ts,tsx,html,css,scss}' --quiet --fix",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx,html,css,scss}' --quiet --fix",
"e2e": "ng e2e"
},
"prettier": {
@@ -41,11 +41,8 @@
]
},
"devDependencies": {
"@angular-eslint/builder": "~21.1.0",
"@angular-eslint/eslint-plugin": "~21.1.0",
"@angular-eslint/eslint-plugin-template": "~21.1.0",
"@angular-eslint/schematics": "~21.1.0",
"@angular-eslint/template-parser": "~21.1.0",
"@angular-eslint/builder": "~21.4.0",
"@angular-eslint/schematics": "~21.4.0",
"@angular/build": "^21.2.19",
"@angular/cli": "^21.2.19",
"@angular/common": "^21.2.19",

View File

@@ -8,7 +8,6 @@ import { lucideIconTemplate } from './lucide-icon-template';
* @internal
*/
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'svg[lucideIcon]',
template: lucideIconTemplate,
host: {

View File

@@ -7,7 +7,7 @@ import { toKebabCase } from './utils/toKebabCase';
export default (iconName: string, iconNode: IconNode): AstroComponentFactory => {
const Component = createComponent(
($$result, $$props: Record<string, any>, $$slots) => {
($$result, $$props: Record<string, unknown>, $$slots) => {
const { class: className, ...restProps } = $$props;
return render`${renderComponent(
$$result,

View File

@@ -1,7 +1,7 @@
import type { HTMLAttributes } from 'astro/types';
// Type that the Astro language server needs to infer component props in Astro files
export type AstroComponent = (_props: IconProps) => any;
export type AstroComponent = (_props: IconProps) => unknown;
export interface IconProps extends SVGAttributes {
color?: string;

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import base64SVG from '@lucide/build-icons/utils/base64SVG';
export default async ({

View File

@@ -16,7 +16,7 @@ export default async function copyIcons(
if (!existsSync(iconsDirectory)) {
mkdirSync(iconsDirectory);
}
// eslint-disable-next-line arrow-body-style
const writeIconPromises = parsedSvgs.map(({ name, contents }) => {
let content = `<!-- ${license} -->\n${contents}`;
content = content.replace('<svg', `<svg\n class="lucide lucide-${name}"`);

View File

@@ -1,4 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
/// <reference types="node" />
import { basename } from 'path';
import { readSvg } from '@lucide/helpers';
import { type INode, parseSync } from 'svgson';

View File

@@ -9,7 +9,7 @@ export interface LucideProps extends Partial<React.SVGProps<SVGSVGElement>> {
export declare const createLucideIcon: (
iconName: string,
iconNode: any[],
iconNode: [elementName: string, attrs: Record<string, string>][],
) => (props: LucideProps) => JSX.Element;
export type Icon = React.FC<LucideProps>;

View File

@@ -1,4 +1,4 @@
// eslint-disable
// This file is make sure that we can import from "lucide-react/dynamic" without having to specify the extension. This is a workaround for the fact that some bundlers (like Webpack) don't support importing from "lucide-react/dynamic.mjs" without specifying the extension.
// Unfortunately, we can't use exports field in package.json because it breaks some runtime environments. See https://github.com/lucide-icons/lucide/issues/2743#issuecomment-2626784300
// eslint-disable-next-line import/no-unresolved
export * from './dynamic.mjs';

View File

@@ -1,4 +1,3 @@
import { JSX } from 'solid-js/jsx-runtime';
import { SVGAttributes } from './types';
const defaultAttributes: SVGAttributes = {

View File

@@ -2,8 +2,6 @@ import { defineConfig } from 'vitest/config'
import solidPlugin from 'vite-plugin-solid';
export default defineConfig({
// TODO: Remove this when Solid testing library has support for Vitest 1.0, see: https://github.com/solidjs/solid-testing-library/issues/52
// @ts-ignore
plugins: [solidPlugin()],
test: {
globals: true,

View File

@@ -1,6 +1,5 @@
import fs from 'fs';
import path from 'path';
import { parseSync } from 'svgson';
import { readSvgDirectory, getCurrentDirPath } from '@lucide/helpers';
import readSvgs from './readSvgs.mts';

View File

@@ -16,7 +16,7 @@ export default async function copyIcons(
if (!existsSync(iconsDirectory)) {
mkdirSync(iconsDirectory);
}
// eslint-disable-next-line arrow-body-style
const writeIconPromises = parsedSvgs.map(({ name, contents }) => {
let content = `<!-- ${license} -->\n${contents}`;
content = content.replace('<svg', `<svg\n class="lucide lucide-${name}"`);

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import { type INode, stringify } from 'svgson';
import { format } from 'prettier';
import { appendFile } from '@lucide/helpers';

View File

@@ -1,4 +1,4 @@
/* eslint-disable import/no-extraneous-dependencies */
/// <reference types="node" />
import { basename } from 'path';
import { readSvg } from '@lucide/helpers';
import { type INode, parseSync } from 'svgson';

View File

@@ -13,7 +13,6 @@ export default defineExportTemplate(async ({
const svgBase64 = base64SVG(svgContents);
return `
import defaultAttributes from '../defaultAttributes';
import type { IconNode } from '../types';
/**

View File

@@ -1,6 +1,6 @@
type Booleanish = boolean | 'true' | 'false';
interface AriaAttributes {
export interface AriaAttributes {
'aria-activedescendant'?: string | undefined;
'aria-atomic'?: Booleanish | undefined;
'aria-autocomplete'?: 'none' | 'inline' | 'list' | 'both' | undefined;

View File

@@ -3,7 +3,7 @@ import defaultAttributes from './defaultAttributes';
import { Icons, SVGProps } from './types';
import { hasA11yProp, mergeClasses, toPascalCase } from '@lucide/shared';
export type CustomAttrs = { [attr: string]: any };
export type CustomAttrs = { [attr: string]: unknown };
/**
* Get the attributes of an HTML element.

View File

@@ -4,7 +4,7 @@
* @param {object} props
* @returns {boolean} Whether the component has an accessibility prop
*/
export const hasA11yProp = (props: Record<string, any>) => {
export const hasA11yProp = (props: object) => {
for (const prop in props) {
if (prop.startsWith('aria-') || prop === 'role' || prop === 'title') {
return true;

View File

@@ -14,20 +14,20 @@ const files = await readdir(targetDirectory, {
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' })) as unknown as string;
const ext = path.extname(filepath);
if (/\.(js|mjs|cjs|ts)/.test(ext)) {
// eslint-disable-next-line no-await-in-loop
await writeFile(filepath, jsBanner + contents, { encoding: 'utf-8' });
}
}

View File

@@ -1,8 +1,5 @@
import fs from 'fs';
import pkg from '../package.json' with { type: 'json' };
const license = fs.readFileSync('LICENSE', 'utf-8');
export function getHTMLBanner() {
return `\
<!--

View File

@@ -30,9 +30,10 @@ export type IconProps = LucideProps;
export type LucideIcon = Component<LucideProps>;
export type IconEvents = {
[evt: string]: CustomEvent<any>;
[evt: string]: CustomEvent<unknown>;
};
export type IconSlots = {
default: {};
// The default slot does not expose any slot props.
default: Record<string, never>;
};

View File

@@ -1,4 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
// eslint-disable-next-line import-x/no-extraneous-dependencies
import { sveltePreprocess } from 'svelte-preprocess';
export default {

1588
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,101 +0,0 @@
import getArgumentOptions from 'minimist';
import githubApi from './githubApi.mts';
const fetchCompareTags = (oldTag: string) =>
githubApi(`https://api.github.com/repos/lucide-icons/lucide/compare/${oldTag}...main`);
const iconRegex = /icons\/(.*)\.svg/g;
const iconTemplate = ({ name, pullNumber, author }: { name: string, pullNumber: number, author: string }) =>
`- \`${name}\` (${pullNumber}) by @${author}`;
const topics = [
{
title: 'New icons 🎨',
template: iconTemplate,
filter: ({ status, filename }) => status === 'added' && filename.match(iconRegex),
},
{
title: 'Modified Icons 🔨',
template: iconTemplate,
filter: ({ status, filename }) => status === 'modified' && filename.match(iconRegex),
},
{
title: 'Code improvements ⚡',
template: ({ title, pullNumber, author }) => `- ${title} (${pullNumber}) by @${author}`,
filter: ({ filename }, index, self) =>
!filename.match(iconRegex) && self.indexOf(filename) === index,
},
];
const fetchCommits = async (file) => {
const commits = await githubApi(
`https://api.github.com/repos/lucide-icons/lucide/commits?path=${file.filename}`,
);
return { ...file, commits };
};
const cliArguments = getArgumentOptions(process.argv.slice(2));
// eslint-disable-next-line func-names
(async function () {
try {
const output = await fetchCompareTags(cliArguments['old-tag']);
if (output?.files == null) {
throw new Error('Tag not found!');
}
const changedFiles = output.files.filter(
({ filename }: { filename: string }) => !filename.match(/docs\/(.*)|(.*)package\.json|tags.json/g),
);
const commits = await Promise.all(changedFiles.map(fetchCommits));
if (!commits.length) {
throw new Error('No commits found');
}
const mappedCommits = commits
.map(({ commits: [pr], filename, sha, status }) => {
const pullNumber = /(.*)\((#[0-9]*)\)/gm.exec(pr.commit.message);
const nameRegex = /^\/?(.+\/)*(.+)\.(.+)$/g.exec(filename);
if (!pr.author) {
// Most likely bot commit
return null;
}
return {
filename,
name: nameRegex && nameRegex[2] ? nameRegex[2] : null,
title: pullNumber && pullNumber[1] ? pullNumber[1].trim() : null,
pullNumber: pullNumber && pullNumber[2] ? pullNumber[2].trim() : null,
author: pr.author?.login || 'unknown',
sha,
status,
};
})
.filter((commit): commit is NonNullable<typeof commit> => Boolean(commit))
.filter(({ pullNumber }) => !!pullNumber);
const changelog = topics.map(({ title, filter, template }) => {
const lines = mappedCommits.filter(filter).map<string>(template);
if (lines.length) {
return [`## ${title}`, ' ', ...lines, ' '];
}
return [''];
});
const changelogMarkown = changelog.flat().join('\n');
console.log(changelogMarkown);
} catch (error) {
throw new Error(error);
}
})().catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -11,7 +11,7 @@ async function main() {
const svgFiles = await readSvgDirectory(ICONS_DIR);
const iconNames = svgFiles.map((icon) => icon.split('.')[0]).reverse();
const argv = (yargs(hideBin(process.argv))
// @ts-ignore
// @ts-expect-error the builder callback overload is not picked up for a default command
.usage('$0 <pattern> <replacement>', 'Renames all icons matching a pattern', (yargs) => {
yargs
.positional('pattern', {

View File

@@ -59,7 +59,7 @@ const getContributors = async (file: string, includeCoAuthors?: boolean) => {
const matches = commit.body.matchAll(
/(^Author:|^Co-authored-by:)\s+(?<author>[^<]+)\s+<(?<email>[^>]+)>/gm,
);
// eslint-disable-next-line no-restricted-syntax
for (const match of matches) {
if (!emails.has(match.groups?.email) && cache.has(match.groups?.email)) {
emails.set(match.groups?.email, Promise.resolve(cache.get(match.groups?.email)));

View File

@@ -27,8 +27,8 @@ export async function allocateCodePoints({
const latestCodePoints = await getLatestCodePoints();
let maxCodePoint = Math.max(...Object.values(latestCodePoints));
let codePointMap = new Map<number, string>();
let newCodePoints: CodePoints = {};
const codePointMap = new Map<number, string>();
const newCodePoints: CodePoints = {};
for (const [iconName, aliases] of iconsWithAliases) {
let codePoint: number | null = null;

View File

@@ -14,7 +14,9 @@ export async function outlineSVG({ iconsDir, outlinedDir, iconsWithAliases }: Ou
try {
try {
await fs.mkdir(outlinedDir);
} catch (error) {} // eslint-disable-line no-empty
} catch (err) {
console.warn(`Directory ${outlinedDir} already exists. Skipping creation.`, err);
}
await SVGFixer(iconsDir, outlinedDir, {
showProgressBar: true,

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import fs from 'fs/promises';
import path from 'path';

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import { hash } from './hash.ts';
/**

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import path from 'path';
import { fileURLToPath } from 'url';

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import { generateHashedKey } from './generateHashedKey.ts';
/**

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
/**
* djb2 hashing function
*
@@ -10,10 +9,8 @@ export const hash = (string: string, seed: number = 5381): string => {
let i = string.length;
while (i) {
// eslint-disable-next-line no-bitwise, no-plusplus
seed = (seed * 33) ^ string.charCodeAt(--i);
}
// eslint-disable-next-line no-bitwise
return (seed >>> 0).toString(36).substr(0, 6);
};

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
/**
* Merge two arrays and remove duplicates
*

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
/**
* Minifies SVG
*

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import fs from 'fs/promises';
import path from 'path';

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import fs from 'fs/promises';
import path from 'path';

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import fs from 'fs/promises';
import path from 'path';

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import { type PathLike } from 'fs';
import fs from 'fs/promises';
import path from 'path';

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import fs from 'fs/promises';
import path from 'path';

View File

@@ -1,10 +1,8 @@
/* eslint-disable import/prefer-default-export */
/**
* @param {array} array
* @returns {array}
*/
export const shuffleArray = <T>(array: T[]): T[] => {
// eslint-disable-next-line no-plusplus
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
/**
* Converts string to CamelCase
*

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
/**
* Converts string to KebabCase
*

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import { toCamelCase } from './toCamelCase.ts';
/**

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import fs from 'fs/promises';
import path from 'path';

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import fs from 'fs';
import path from 'path';
import { writeFile } from './writeFile.ts';

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import fs from 'fs/promises';
import path from 'path';

View File

@@ -9,6 +9,7 @@
"target": "ESNext",
"esModuleInterop": true,
"lib": ["esnext"],
"types": ["node"],
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"noEmit": true,

View File

@@ -1,6 +1,5 @@
import path from 'path';
import fs from 'fs';
// eslint-disable-next-line import/no-extraneous-dependencies
import { toPascalCase, resetFile, appendFile } from '@lucide/helpers';
import deprecationReasonTemplate from '../../utils/deprecationReasonTemplate.ts';
import getExportString from './getExportString.ts';

View File

@@ -9,7 +9,6 @@ import generateIconFiles from './building/generateIconFiles.ts';
import generateExportsFile from './building/generateExportsFile.ts';
import generateAliasesFiles from './building/aliases/generateAliasesFiles.ts';
// eslint-disable-next-line import/no-named-as-default, import/no-named-as-default-member
import getIconMetaData from './utils/getIconMetaData.ts';
import generateDynamicImports from './building/generateDynamicImports.ts';

View File

@@ -9,6 +9,7 @@
"target": "ESNext",
"esModuleInterop": true,
"lib": ["esnext"],
"types": ["node"],
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"noEmit": true,

View File

@@ -1,5 +1,3 @@
import { type INode } from 'svgson';
export type SVGProps = Record<string, string | number>;
export type IconNode = [tag: string, attrs: SVGProps][];

View File

@@ -9,8 +9,6 @@ export default function deprecationReasonTemplate(
iconName: string;
},
) {
const resourceName = deprecationReason.startsWith('icon') ? 'icon' : 'alias';
switch (deprecationReason) {
case 'alias.typo':
return `Renamed because of typo, use {@link ${componentName}} instead.`;

View File

@@ -1,4 +1,3 @@
/* eslint-disable import/no-extraneous-dependencies */
import { visualizer } from 'rollup-plugin-visualizer';
import replace from '@rollup/plugin-replace';
import license from 'rollup-plugin-license';