fix(@lucide/lab): Fix lab build (#4618)

* chore(lab): Adjust build scripts

* add check workflow

* Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* update gitignore

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
Eric Fennis
2026-07-31 09:11:20 +02:00
committed by GitHub
parent 240118e521
commit 1502285072
15 changed files with 98 additions and 69 deletions

30
.github/workflows/lucide-lab.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Lucide Lab checks
on:
pull_request:
paths:
- packages/lab/**
- packages/shared/**
- tools/build-icons/**
- tools/rollup-plugins/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm --filter @lucide/lab build

1
.gitignore vendored
View File

@@ -61,4 +61,3 @@ docs/.vercel
docs/.nitro
docs/public/og/**
!docs/public/og/general.png
.gitignore

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

@@ -0,0 +1,2 @@
icons
icon-nodes.json

View File

@@ -27,17 +27,17 @@
"scripts": {
"copy:license": "cp ../../LICENSE ./LICENSE",
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm build:bundle && pnpm build:lib",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mjs --iconFileExtension=.ts --withAliases --exportModuleNameCasing=camel --renderUniqueKey --aliasNamesOnly --aliasesFileExtension=.ts --exportFileName=index.ts",
"build:lib": "node ./scripts/buildLib.mjs",
"build:icons": "build-icons --input=../../lab --output=./src --templateSrc=./scripts/exportTemplate.mts --iconFileExtension=.ts --exportModuleNameCasing=camel --renderUniqueKey --exportFileName=index.ts",
"build:lib": "node ./scripts/buildLib.mts",
"build:bundle": "rollup -c rollup.config.mjs",
"clean": "rm -f src/icons/*.ts && rm -rf dist",
"clean": "rm -f src/icons/*.ts && rm -rf dist && rm -rf icons && rm -f icon-nodes.json",
"version": "pnpm version --git-tag-version=false"
},
"devDependencies": {
"@lucide/build-icons": "workspace:*",
"@lucide/helpers": "workspace:*",
"@lucide/rollup-plugins": "workspace:*",
"minimist": "^1.2.8",
"@types/node": "^22.20.1",
"prettier": "^2.3.2",
"rollup": "^4.9.2",
"rollup-plugin-dts": "^6.1.0",

View File

@@ -1,6 +1,6 @@
import plugins from '@lucide/rollup-plugins';
import dts from 'rollup-plugin-dts';
import pkg from './package.json' assert { type: 'json' };
import pkg from './package.json' with { type: 'json' };
const outputFileName = 'lucide-lab';
const outputDir = 'dist';

View File

@@ -1,40 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
import fs from 'fs';
import path from 'path';
import getArgumentOptions from 'minimist';
import { parseSync } from 'svgson';
import { readSvgDirectory, getCurrentDirPath } from '@lucide/helpers';
import readSvgs from './readSvgs.mjs';
import generateIconNodes from './generateIconNodes.mjs';
import copyIcons from './copyIcons.mjs';
import pkg from '../package.json' assert { type: 'json' };
const cliArguments = getArgumentOptions(process.argv.slice(2));
const createDirectory = (dir) => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
};
const currentDir = getCurrentDirPath(import.meta.url);
const PACKAGE_DIR = path.resolve(currentDir, '../');
const ICONS_DIR = path.join(PACKAGE_DIR, '../../icons');
const license = `@license ${pkg.name} v${pkg.version} - ${pkg.license}`;
const svgFiles = await readSvgDirectory(ICONS_DIR);
const svgs = await readSvgs(svgFiles, ICONS_DIR);
const parsedSvgs = svgs.map(({ name, contents }) => ({
name,
contents,
parsedSvg: parseSync(contents),
}));
await Promise.all([
generateIconNodes(parsedSvgs, PACKAGE_DIR),
copyIcons(parsedSvgs, PACKAGE_DIR, license),
]);

View File

@@ -0,0 +1,20 @@
import path from 'path';
import { readSvgDirectory, getCurrentDirPath } from '@lucide/helpers';
import readSvgs from './readSvgs.mts';
import generateIconNodes from './generateIconNodes.mts';
import copyIcons from './copyIcons.mts';
import pkg from '../package.json' with { type: 'json' };
const currentDir = getCurrentDirPath(import.meta.url);
const PACKAGE_DIR = path.resolve(currentDir, '../');
const ICONS_DIR = path.join(PACKAGE_DIR, '../../lab');
const license = `@license ${pkg.name} v${pkg.version} - ${pkg.license}`;
const svgFiles = await readSvgDirectory(ICONS_DIR);
const svgs = await readSvgs(svgFiles, ICONS_DIR);
await Promise.all([generateIconNodes(svgs, PACKAGE_DIR), copyIcons(svgs, PACKAGE_DIR, license)]);

View File

@@ -1,13 +1,16 @@
import { writeFile } from 'fs/promises';
import { existsSync, unlinkSync, mkdirSync } from 'fs';
import { type SVGFile } from './readSvgs.mts';
export default async function copyIcons(parsedSvgs, packageDir, license) {
export default async function copyIcons(
parsedSvgs: SVGFile[],
packageDir: string,
license: string,
) {
const iconsDirectory = `${packageDir}/icons`;
if (existsSync(iconsDirectory)) {
try {
unlinkSync(iconsDirectory);
} catch (error) {}
unlinkSync(iconsDirectory);
}
if (!existsSync(iconsDirectory)) {

View File

@@ -1,12 +1,13 @@
/* eslint-disable import/no-extraneous-dependencies */
import base64SVG from '@lucide/build-icons/utils/base64SVG.mjs';
import base64SVG from '@lucide/build-icons/utils/base64SVG';
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
import { toCamelCase } from '@lucide/helpers';
export default ({ componentName, iconName, children, getSvg, deprecated }) => {
const svgContents = getSvg();
const svgBase64 = base64SVG(svgContents);
export default defineExportTemplate(
async ({ componentName, iconName, children, getSvg, deprecated, deprecationReason }) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
return `
return `
import type { IconNode } from '../types';
/**
@@ -17,10 +18,11 @@ import type { IconNode } from '../types';
* @see https://lucide.dev/guide/packages/lucide - Documentation
*
* @returns {Array}
* ${deprecated ? '@deprecated' : ''}
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
const ${toCamelCase(componentName)}: IconNode = ${JSON.stringify(children)};
export default ${toCamelCase(componentName)};
`;
};
},
);

View File

@@ -1,11 +1,12 @@
import { writeFile } from '@lucide/helpers';
import { type SVGFile } from './readSvgs.mts';
export default async function generateIconNodes(parsedSvgs, packageDir) {
export default async function generateIconNodes(parsedSvgs: SVGFile[], packageDir: string) {
const iconNodes = parsedSvgs.reduce((acc, { name, parsedSvg }) => {
acc[name] = parsedSvg.children.map(({ name, attributes }) => [name, attributes]);
return acc;
}, {});
}, {} as Record<string, [string, Record<string, unknown> | undefined][]>);
const iconNodesStringified = JSON.stringify(iconNodes, null, 2);

View File

@@ -1,6 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import { basename } from 'path';
import { readSvg } from '@lucide/helpers';
import { type INode, parseSync } from 'svgson';
/**
* Build an object in the format: `{ <name>: <contents> }`.
@@ -8,13 +9,23 @@ import { readSvg } from '@lucide/helpers';
* @param {Function} getSvg - A function that returns the contents of an SVG file given a filename.
* @returns {Object}
*/
export default async function readSVGs(svgFiles, iconsDirectory) {
const svgReadPromises = svgFiles.map(async (svgFile) => {
export default function readSVGs(svgFiles: string[], iconsDirectory: string) {
const SVGReadPromises = svgFiles.map(async (svgFile) => {
const name = basename(svgFile, '.svg');
const contents = await readSvg(svgFile, iconsDirectory);
return { name, contents };
return {
name,
contents,
parsedSvg: parseSync(contents),
};
});
return Promise.all(svgReadPromises);
return Promise.all(SVGReadPromises);
}
export type SVGFile = {
name: string;
contents: string;
parsedSvg: INode;
};

View File

@@ -1,2 +1 @@
export * from './icons';
export * from './aliases';

View File

@@ -11,6 +11,7 @@
"allowJs": true,
"lib": ["esnext", "dom"],
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"sourceMap": true,
"outDir": "./dist",
},

6
pnpm-lock.yaml generated
View File

@@ -406,9 +406,9 @@ importers:
'@lucide/rollup-plugins':
specifier: workspace:*
version: link:../../tools/rollup-plugins
minimist:
specifier: ^1.2.8
version: 1.2.8
'@types/node':
specifier: ^22.20.1
version: 22.20.1
prettier:
specifier: ^2.3.2
version: 2.8.8

View File

@@ -34,11 +34,12 @@ interface CliArguments {
useDefaultExports?: boolean;
pretty?: boolean;
output: string | undefined;
input: string | undefined;
}
const cliArguments = getArgumentOptions(process.argv.slice(2)) as unknown as CliArguments;
const ICONS_DIR = path.resolve(process.cwd(), '../../icons');
const ICONS_DIR = path.resolve(process.cwd(), cliArguments.input || '../../icons');
const OUTPUT_DIR = path.resolve(process.cwd(), cliArguments.output || '../build');
if (!fs.existsSync(OUTPUT_DIR)) {