Featherity Npm package, reorganize scripting. (#52)

* New setup for new NPM package

* Add build scripts for dist

* Add introduction readme

* Refactor names

* update package.json

* remove log

* rename variable

* Factoring

* Improve optimize script

* Add eslint config

* Eslint fixes

* rename import

* Move packeges

* Setup rollup and build progress

* Refactor scripts

* fix lint error

* remove lint disabler

* Bring back old libraries

* add indentation

* reset packages directory

* remove vscode setting files

* 0.1.0-alpha.0

* new version

* 0.1.0-alpha.1

* Fix build process

* Add create element to the entry file

* update version number

* publish new alhpa version

* fixing bugs

* Add jest and tests

* replace with XML createElement

* set new version

* Fix svg generation

* Add tests for main library

* Update docs

* Adjust tests and selectors

* update the spec

* Update README.md

* Update README.md

* Update README.md

* update version

* Update README.md

* Move function to helpers file

* rename license, package and readme

* Fix build files

* rename packages

Co-authored-by: Eric Fennis <eric.fennis@endurance.com>
This commit is contained in:
Eric Fennis
2020-10-06 20:23:26 +02:00
committed by GitHub
parent 8b5278437a
commit 11c6a2e917
38 changed files with 4953 additions and 3858 deletions

11
.editorconfig Normal file
View File

@@ -0,0 +1,11 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
quote_type = single
max_line_length = 100

5
.eslintignore Normal file
View File

@@ -0,0 +1,5 @@
dist
build
coverage
lib
tests

21
.eslintrc.json Normal file
View File

@@ -0,0 +1,21 @@
{
"env": {
"browser": true,
"node": true
},
"extends": ["airbnb-base", "prettier"],
"plugins": ["import", "prettier"],
"rules": {
"no-console": "off",
"no-param-reassign": "off",
"no-shadow": "off",
"no-use-before-define": "off",
"prettier/prettier": [
"error",
{
"singleQuote": true,
"trailingComma": "all"
}
]
}
}

8
.gitignore vendored
View File

@@ -2,3 +2,11 @@
.next
.now
node_modules
dist
build
lib
sandbox
stash
coverage
stats
*.log

4
.npmignore Normal file
View File

@@ -0,0 +1,4 @@
.github
packages
stats
build

View File

@@ -21,4 +21,4 @@ Guidelines for pull requests:
Before creating an icon request, please search to see if someone has requested the icon already. If there is an open request, please add a :+1:.
If the icon has not already been requested, [create an issue](https://github.com/featherity/featherity/issues/new?title=Icon%20Request:) with a title of `Icon request: <icon name>` and add as much information as possible.
If the icon has not already been requested, [create an issue](https://github.com/lucide-icons/lucide/issues/new?title=Icon%20Request:) with a title of `Icon request: <icon name>` and add as much information as possible.

View File

@@ -1,6 +1,6 @@
ISC License
Copyright (c) 2020, Featherity Contributors
Copyright (c) 2020, Lucide Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above

132
README.md
View File

@@ -1,10 +1,10 @@
# Featherity
# Lucide
[![Discord](https://img.shields.io/discord/723074157486800936?label=chat&logo=discord&logoColor=%23ffffff&colorB=%237289DA)](https://discord.gg/EH6nSts)
## What is Featherity?
## What is Lucide?
Featherity is a community-run fork of [Feather Icons](https://github.com/feathericons/feather), open for anyone to contribute icons.
Lucide is a community-run fork of [Feather Icons](https://github.com/feathericons/feather), open for anyone to contribute icons.
Note that we are completely independent from Feather, so **icons submitted here won't get added to Feather Icons or its associated librairies**.
@@ -15,11 +15,127 @@ Note that we are completely independent from Feather, so **icons submitted here
* [Contributing](#contributing)
* [License](#license)
## Installation
### Package Managers
``` bash
npm install lucide
#or
yarn add lucide
```
### CDN
``` html
<!-- Development version -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<!-- Production version -->
<script src="https://unpkg.com/lucide@latest"></script>
```
## Usage
At its core, Featherity is a collection of [SVG](https://svgontheweb.com/#svg) files. This means that you can use Feather icons in all the same ways you can use SVGs (e.g. `img`, `background-image`, `inline`, `object`, `embed`, `iframe`). Here's a helpful article detailing the many ways SVGs can be used on the web: [SVG on the Web Implementation Options](https://svgontheweb.com/#implementation)
At its core, Lucide is a collection of [SVG](https://svgontheweb.com/#svg) files. This means that you can use Feather icons in all the same ways you can use SVGs (e.g. `img`, `background-image`, `inline`, `object`, `embed`, `iframe`). Here's a helpful article detailing the many ways SVGs can be used on the web: [SVG on the Web Implementation Options](https://svgontheweb.com/#implementation)
The following are additional ways you can use Featherity.
The following are additional ways you can use Lucide.
With the Javascript library you can easily incorporate the icon you want in your webpage.
### With unpkg
Here is a complete example with unpkg
```html
<!DOCTYPE html>
<body>
<i icon-name="volume-2" class="my-class"></i>
<i icon-name="x"></i>
<i icon-name="menu"></i>
<script src="https://unpkg.com/lucide@latest"></script>
<script>
lucide.createIcons();
</script>
</body>
```
### With ESModules
To reduce bundle size, lucide is build to be fully threeshakeble.
The `createIcons` function will search for HTMLElements with the attribute `icon-name` and replace it with the svg from the given icon name.
```html
<!-- Your HTML file -->
<i icon-name="menu"></i>
```
```js
import { createIcons, icons } from 'lucide';
// Caustion, this will import all the icons and bundle them.
createIcons({icons});
// Recommended way, to include only the icons you need.
import { createIcons, Menu, ArrowRight, Globe } from 'lucide';
createIcons({
icons: {
Menu,
ArrowRight,
Globe,
},
});
```
#### Additional Options
In the `createIcons` function you can pass some extra parameters to adjust the `nameAttr` or add custom attributes like for example classes.
Here is a full example:
```js
import { createIcons } from 'lucide';
createIcons({
attrs: {
class: ['my-custom-class', 'icon'],
'stroke-width': 1,
stroke: '#333',
},
nameAttr: 'icon-name', // atrribute for the icon name.
});
```
#### Threeshake the library, only use the icons you use
```js
import { createIcons, Menu, ArrowRight, Globe } from 'lucide';
createIcons({
icons: {
Menu,
ArrowRight,
Globe,
},
});
```
#### Custom Element binding
```js
import { createElement, Menu } from 'lucide';
const menuIcon = createElement(Menu); // Returns HTMLElement (svg)
// set custom attributes with browser native functions
menuIcon.setAttribute('stroke', '#333');
menuIcon.classList.add('my-icon-class');
// Append HTMLElement in webpage
const myApp = document.getElementById('app');
myApp.appendChild(menuIcon);
```
### Figma
@@ -27,9 +143,9 @@ You can use the components from [this Figma file](https://www.figma.com/file/g0U
## Contributing
For more info on how to contribute please see the [contribution guidelines](https://github.com/featherity/featherity/blob/master/CONTRIBUTING.md).
For more info on how to contribute please see the [contribution guidelines](https://github.com/lucide-icons/lucide/blob/master/CONTRIBUTING.md).
Caught a mistake or want to contribute to the documentation? [Edit this page on Github](https://github.com/featherity/featherity/blob/master/README.md)
Caught a mistake or want to contribute to the documentation? [Edit this page on Github](https://github.com/lucide-icons/lucide/blob/master/README.md)
## Community
@@ -37,4 +153,4 @@ Join the community on our [Discord](https://discord.gg/EH6nSts) server!
## License
Featherity is licensed under the [ISC License](https://github.com/featherity/featherity/blob/master/LICENSE).
Lucide is licensed under the [ISC License](https://github.com/lucide-icons/lucide/blob/master/LICENSE).

27
babel.config.js Normal file
View File

@@ -0,0 +1,27 @@
module.exports = {
presets: [
[
'@babel/env',
{
loose: true,
modules: false,
},
],
],
env: {
test: {
presets: ['@babel/env'],
plugins: ['@babel/plugin-transform-runtime'],
},
dev: {
plugins: [
[
'transform-inline-environment-variables',
{
include: ['NODE_ENV'],
},
],
],
},
},
};

14
jest.config.js Normal file
View File

@@ -0,0 +1,14 @@
const esModules = ['lodash-es'].join('|');
module.exports = {
verbose: true,
roots: ['<rootDir>/src/', '<rootDir>/tests/'],
moduleFileExtensions: ['js'],
transformIgnorePatterns: [`/node_modules/(?!${esModules})`],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.js$': 'babel-jest',
},
};

View File

@@ -1,12 +1,63 @@
{
"private": true,
"name": "lucide",
"amdName": "lucide",
"homepage": "https://featherity.netlify.app",
"repository": "github:lucide-icons/lucide",
"url" : "https://github.com/owner/project/issues",
"license": "ISC",
"version": "0.1.0",
"source": "build/lucide.js",
"main": "dist/cjs/lucide.js",
"main:umd": "dist/umd/lucide.js",
"module": "lib/lucide.js",
"unpkg": "dist/umd/lucide.min.js",
"sideEffects": false,
"scripts": {
"react:compile": "yarn workspace react compile",
"site:dev": "yarn workspace site dev",
"site:deploy": "yarn workspace site deploy"
"start": "babel-watch --watch src",
"clean": "rimraf lib && rimraf dist && rimraf build",
"build": "yarn clean && yarn build:move && yarn build:icons && yarn build:es && yarn build:esbrowser && yarn build:bundles",
"build:move": "cp -av src build",
"build:icons": "npx babel-node ./scripts/buildIcons.js --presets @babel/env",
"build:es": "babel build -d lib --source-maps --ignore '**/*.test.js','**/__mocks__'",
"build:esbrowser": "BROWSER_COMPAT=true yarn build:es -d dist/esm",
"build:bundles": "BROWSER_COMPAT=true rollup -c rollup.config.js",
"optimize": "npx babel-node ./scripts/optimizeSvgs.js --presets @babel/env",
"test": "jest"
},
"workspaces": [
"packages/react",
"packages/site"
]
"devDependencies": {
"@ampproject/rollup-plugin-closure-compiler": "^0.25.2",
"@atomico/rollup-plugin-sizes": "^1.1.4",
"@babel/cli": "^7.10.5",
"@babel/core": "^7.11.1",
"@babel/node": "^7.10.5",
"@babel/preset-env": "^7.11.0",
"@rollup/plugin-babel": "^5.0.0",
"babel-jest": "^26.3.0",
"babel-plugin-add-import-extension": "^1.4.3",
"cheerio": "^1.0.0-rc.2",
"eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.5.0",
"eslint-plugin-prettier": "^2.5.0",
"html-minifier": "^3.5.8",
"jest": "^26.4.2",
"lodash": "^4.17.19",
"prettier": "^1.8.2",
"rollup": "^2.7.3",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-license": "^2.0.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^5.2.0",
"rollup-plugin-visualizer": "^4.1.0",
"svgo": "^1.3.2"
},
"dependencies": {
"@babel/plugin-transform-runtime": "^7.11.5",
"core-js": "3",
"htmlparser2": "^4.1.0",
"lodash-es": "^4.17.15",
"prop-types": "^15.7.2"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "react-featherity",
"name": "lucide-react",
"version": "1.0.0",
"description": "React component for Featherity icons",
"description": "React component for lucide icons",
"main": "src/index.js",
"typings": "src/index.d.ts",
"author": "John Letey",

View File

@@ -38,7 +38,7 @@ const Layout = ({ children }) => {
</Text>
</Flex>
<Flex justifyContent="center" alignItems="center">
<Link href="https://github.com/featherity/featherity" isExternal style={{ fontSize: "18px", marginRight: '24px' }}>
<Link href="https://github.com/lucide-icons/lucide" isExternal style={{ fontSize: "18px", marginRight: '24px' }}>
Github
</Link>
<div onClick={toggleColorMode} style={{ cursor: "pointer" }}>

77
rollup.config.js Normal file
View File

@@ -0,0 +1,77 @@
import babel from '@rollup/plugin-babel';
import bundleSize from '@atomico/rollup-plugin-sizes';
import compiler from '@ampproject/rollup-plugin-closure-compiler';
import { terser } from 'rollup-plugin-terser';
import visualizer from 'rollup-plugin-visualizer';
import license from 'rollup-plugin-license';
import replace from 'rollup-plugin-replace';
import resolve from 'rollup-plugin-node-resolve';
import commonJS from 'rollup-plugin-commonjs';
import pkg from './package.json';
const outputFileName = pkg.name;
const inputs = ['build/lucide.js'];
const bundles = [
{
inputs,
format: 'umd',
dir: 'dist',
minify: true,
},
{
inputs,
format: 'umd',
dir: 'dist',
},
{
inputs,
format: 'cjs',
dir: 'dist',
},
];
const configs = bundles
.map(({ inputs, dir, format, minify }) =>
inputs.map(input => ({
input,
external: ['lodash/camelCase', 'lodash/upperFirst'],
plugins: [
replace({
'icons = {}': 'icons = allIcons',
delimiters: ['', ''],
}),
babel({
babelHelpers: 'bundled',
}),
// The two minifiers together seem to procude a smaller bundle 🤷‍♂️
minify && compiler(),
minify && terser(),
license({
banner: `${pkg.name} v${pkg.version} - ${pkg.license}`,
}),
bundleSize(),
resolve(),
commonJS({
include: 'node_modules/**',
}),
visualizer({
sourcemap: true,
filename: `stats/${outputFileName}${minify ? '-min' : ''}.html`,
}),
].filter(Boolean),
output: {
name: 'lucide',
file: `${dir}/${format}/${outputFileName}${minify ? '.min' : ''}.js`,
format,
sourcemap: true,
globals: {
'lodash/camelCase': 'camelCase',
'lodash/upperFirst': 'upperFirst',
},
},
})),
)
.flat();
export default configs;

View File

@@ -0,0 +1,23 @@
import path from 'path';
import { generateComponentName, resetFile, appendFile } from '../helpers';
export default function(inputEntry, outputDirectory, componentGetter, iconNodes) {
const fileName = path.basename(inputEntry);
// Reset file
resetFile(fileName, outputDirectory);
const icons = Object.keys(iconNodes);
// Generate Import for Icon VNodes
icons.forEach(iconName => {
const componentName = generateComponentName(iconName);
const importString = `export { default as ${componentName} } from './${iconName}';\n`;
appendFile(importString, fileName, outputDirectory);
});
appendFile('\n', fileName, outputDirectory);
console.log(`Successfully generated ${fileName} file`);
}

View File

@@ -0,0 +1,27 @@
/* eslint-disable import/no-extraneous-dependencies */
import fs from 'fs';
import path from 'path';
import prettier from 'prettier';
import { generateComponentName } from '../helpers';
export default function(iconNode, outputDirectory, template) {
const icons = Object.keys(iconNode);
const iconsDistDirectory = path.join(outputDirectory, `icons`);
if (!fs.existsSync(iconsDistDirectory)) {
fs.mkdirSync(iconsDistDirectory);
}
icons.forEach(icon => {
const location = path.join(iconsDistDirectory, `${icon}.js`);
const componentName = generateComponentName(icon);
const node = JSON.stringify(iconNode[icon]);
const elementTemplate = template({ componentName, node });
fs.writeFileSync(location, prettier.format(elementTemplate, { parser: 'babel' }), 'utf-8');
console.log('Successfully built', componentName);
});
}

42
scripts/buildIcons.js Normal file
View File

@@ -0,0 +1,42 @@
import fs from 'fs';
import path from 'path';
import renderIconsObject from './render/renderIconsObject';
import renderIconNodes from './render/renderIconNodes';
import generateIconFiles from './build/generateIconFiles';
import generateExportsFile from './build/generateExportsFile';
import { readSvgDirectory } from './helpers';
const ICONS_DIR = path.resolve(__dirname, '../icons');
const OUTPUT_DIR = path.resolve(__dirname, '../build');
const SRC_DIR = path.resolve(__dirname, '../src');
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR);
}
const svgFiles = readSvgDirectory(ICONS_DIR);
const icons = renderIconsObject(svgFiles, ICONS_DIR);
const iconVNodes = renderIconNodes(icons);
// Generates iconsNodes files for each icon
generateIconFiles(
iconVNodes,
OUTPUT_DIR,
({ componentName, node }) => `
const ${componentName} = ${node};
export default ${componentName};
`,
);
// Generates entry files for the compiler filled with icons exports
generateExportsFile(
path.join(SRC_DIR, 'icons/index.js'),
path.join(OUTPUT_DIR, 'icons'),
'getElement',
iconVNodes,
);

74
scripts/helpers.js Normal file
View File

@@ -0,0 +1,74 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { upperFirst, camelCase } from 'lodash/string';
import fs from 'fs';
import path from 'path';
/**
* Generates a componentName of a String.
*
* @param {string} iconName
*/
export const generateComponentName = iconName =>
iconName === 'github' ? 'GitHub' : upperFirst(camelCase(iconName));
/**
* Resets the file contents.
*
* @param {string} fileName
* @param {string} outputDirectory
*/
export const resetFile = (fileName, outputDirectory) =>
fs.writeFileSync(path.join(outputDirectory, fileName), '', 'utf-8');
/**
* Reads the file contents.
*
* @param {string} path
*/
export const readFile = entry => fs.readFileSync(path.resolve(__dirname, '../', entry), 'utf-8');
/**
* append content to a file
*
* @param {string} content
* @param {string} fileName
* @param {string} outputDirectory
*/
export const appendFile = (content, fileName, outputDirectory) =>
fs.appendFileSync(path.join(outputDirectory, fileName), content, 'utf-8');
/**
* writes content to a file
*
* @param {string} content
* @param {string} fileName
* @param {string} outputDirectory
*/
export const writeFile = (content, fileName, outputDirectory) =>
fs.writeFileSync(path.join(outputDirectory, fileName), content, 'utf-8');
/**
* reads the icon directory
*
* @param {string} directory
*/
export const readSvgDirectory = directory =>
fs.readdirSync(directory).filter(file => path.extname(file) === '.svg');
/**
* Read svg from directory
*
* @param {string} fileName
* @param {string} directory
*/
export const readSvg = (fileName, directory) => fs.readFileSync(path.join(directory, fileName));
/**
* writes content to a file
*
* @param {string} fileName
* @param {string} outputDirectory
* @param {string} content
*/
export const writeSvgFile = (fileName, outputDirectory, content) =>
fs.appendFileSync(path.join(outputDirectory, fileName), content, 'utf-8');

15
scripts/optimizeSvgs.js Normal file
View File

@@ -0,0 +1,15 @@
import fs from 'fs';
import path from 'path';
import processSvg from './render/processSvg';
import { readSvgDirectory, writeSvgFile } from './helpers';
const ICONS_DIR = path.resolve(__dirname, '../icons');
console.log(`Optimizing SVGs...`);
const svgFiles = readSvgDirectory(ICONS_DIR);
svgFiles.forEach(svgFile => {
const content = fs.readFileSync(path.join(ICONS_DIR, svgFile));
processSvg(content).then(svg => writeSvgFile(svg, ICONS_DIR, content));
});

View File

@@ -0,0 +1,11 @@
{
"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"
}

View File

@@ -0,0 +1,57 @@
/* eslint-disable import/no-extraneous-dependencies */
import Svgo from 'svgo';
import cheerio from 'cheerio';
import { format } from 'prettier';
import DEFAULT_ATTRS from './default-attrs.json';
/**
* Process SVG string.
* @param {string} svg - An SVG string.
* @param {Promise<string>}
*/
function processSvg(svg) {
return (
optimize(svg)
.then(setAttrs)
.then(format)
// remove semicolon inserted by prettier
// because prettier thinks it's formatting JSX not HTML
.then(svg => svg.replace(/;/g, ''))
);
}
/**
* Optimize SVG with `svgo`.
* @param {string} svg - An SVG string.
* @returns {Promise<string>}
*/
function optimize(svg) {
const svgo = new Svgo({
plugins: [
{ convertShapeToPath: false },
{ mergePaths: false },
{ removeAttrs: { attrs: '(fill|stroke.*)' } },
{ removeTitle: true },
],
});
return new Promise(resolve => {
svgo.optimize(svg, ({ data }) => resolve(data));
});
}
/**
* Set default attibutes on SVG.
* @param {string} svg - An SVG string.
* @returns {string}
*/
function setAttrs(svg) {
const $ = cheerio.load(svg);
Object.keys(DEFAULT_ATTRS).forEach(key => $('svg').attr(key, DEFAULT_ATTRS[key]));
return $('body').html();
}
export default processSvg;

View File

@@ -0,0 +1,29 @@
/* eslint-disable import/no-extraneous-dependencies */
import { parseDOM } from 'htmlparser2';
import DEFAULT_ATTRS from './default-attrs.json';
export default iconsObject => {
const iconNodes = {};
Object.keys(iconsObject).forEach(icon => {
const svgString = iconsObject[icon];
const dom = parseDOM(svgString);
const children = dom.map(element => [
element.name,
{
...element.attribs,
},
]);
iconNodes[icon] = [
'svg',
{
...DEFAULT_ATTRS,
},
children,
];
});
return iconNodes;
};

View File

@@ -0,0 +1,35 @@
/* eslint-disable import/no-extraneous-dependencies */
import path from 'path';
import cheerio from 'cheerio';
import { minify } from 'html-minifier';
import { readSvg } from '../helpers';
/**
* Get contents between opening and closing `<svg>` tags.
* @param {string} svg
* @returns {string}
*/
function getSvgContents(svg) {
const $ = cheerio.load(svg);
return minify($('svg').html(), { collapseWhitespace: true });
}
/**
* Build an object in the format: `{ <name>: <contents> }`.
* @param {string[]} svgFiles - A list of filenames.
* @param {Function} getSvg - A function that returns the contents of an SVG file given a filename.
* @returns {Object}
*/
export default (svgFiles, iconsDirectory) =>
svgFiles
.map(svgFile => {
const name = path.basename(svgFile, '.svg');
const svg = readSvg(svgFile, iconsDirectory);
const contents = getSvgContents(svg);
return { name, contents };
})
.reduce((icons, icon) => {
icons[icon.name] = icon.contents;
return icons;
}, {});

19
src/createElement.js Normal file
View File

@@ -0,0 +1,19 @@
const createElement = (tag, attrs, children = []) => {
const element = document.createElementNS('http://www.w3.org/2000/svg', tag);
Object.keys(attrs).forEach(name => {
element.setAttribute(name, attrs[name]);
});
if (children.length) {
children = children.forEach(child => {
const childElement = createElement(...child);
element.appendChild(childElement);
});
}
return element;
};
export default ([tag, attrs, children]) => createElement(tag, attrs, children);

5
src/icons/index.js Normal file
View File

@@ -0,0 +1,5 @@
/*
Icons exports.
Will be generated
*/

34
src/lucide.js Normal file
View File

@@ -0,0 +1,34 @@
import replaceElement from './replaceElement';
import * as allIcons from './icons/index';
/*
Create icons
*/
export const createIcons = ({ icons = {}, nameAttr = 'icon-name', attrs = {} } = {}) => {
if (!Object.values(icons).length) {
throw new Error(
"Please provide an icons object.\nIf you want to use all the icons you can import it like:\n `import { createIcons, icons } from 'lucide';\nlucide.createIcons({icons});`",
);
}
if (typeof document === 'undefined') {
throw new Error('`createIcons()` only works in a browser environment.');
}
const elementsToReplace = document.querySelectorAll(`[${nameAttr}]`);
Array.from(elementsToReplace).forEach(element =>
replaceElement(element, { nameAttr, icons, attrs }),
);
};
/*
Create Element function export.
*/
export { default as createElement } from './createElement';
/*
Icons exports.
*/
export { allIcons as icons };
export * from './icons/index';

83
src/replaceElement.js Normal file
View File

@@ -0,0 +1,83 @@
import { camelCase, upperFirst } from 'lodash-es';
import createElement from './createElement';
/**
* Get the attributes of an HTML element.
* @param {HTMLElement} element
* @returns {Object}
*/
export const getAttrs = element =>
Array.from(element.attributes).reduce((attrs, attr) => {
attrs[attr.name] = attr.value;
return attrs;
}, {});
/**
* Gets the classNames of an attributes Object.
* @param {Object} attrs
* @returns {Array}
*/
export const getClassNames = attrs => {
if (!attrs || !attrs.class) return '';
if (attrs.class && typeof attrs.class === 'string') {
return attrs.class.split(' ');
}
if (attrs.class && Array.isArray(attrs.class)) {
return attrs.class;
}
return '';
};
/**
* Combines the classNames of array of classNames to a String
* @param {Array} arrayOfClassnames
* @returns {String}
*/
export const combineClassNames = arrayOfClassnames => {
const classNameArray = arrayOfClassnames.flatMap(getClassNames);
return classNameArray
.map(classItem => classItem.trim())
.filter(Boolean)
.join(' ');
};
/**
* ReplaceElement, replaces the given element with the created icon.
* @param {HTMLElement} element
* @param {Object: {String, Array, Object}} options: { nameAttr, icons, attrs }
* @returns {Function}
*/
export default (element, { nameAttr, icons, attrs }) => {
const iconName = element.getAttribute(nameAttr);
const ComponentName = upperFirst(camelCase(iconName));
const iconNode = icons[ComponentName];
if (!iconNode) {
return console.warn(
`${element.outerHTML} icon name was not found in the provided icons object.`,
);
}
const elementAttrs = getAttrs(element);
const [, iconAttrs] = iconNode;
const allAttrs = {
...iconAttrs,
...attrs,
};
iconNode[1] = { ...allAttrs };
const classNames = combineClassNames([iconAttrs, elementAttrs, attrs]);
if (classNames) {
iconNode[1].class = classNames;
}
const svgElement = createElement(iconNode);
return element.parentNode.replaceChild(svgElement, element);
};

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`createIcons should read elements from DOM and replace it with icons 1`] = `"<svg 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\\"><polygon points=\\"11 5 6 9 2 9 2 15 6 15 11 19 11 5\\"></polygon><path d=\\"M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07\\"></path></svg>"`;

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`combineClassNames should retuns a string of classNames 1`] = `"item1 item2 item3 item4 item5 item6 item7 item8 item9"`;

21
tests/icons/download.js Normal file
View File

@@ -0,0 +1,21 @@
const Download = [
"svg",
{
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"
},
[
["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
["polyline", { points: "7 10 12 15 17 10" }],
["line", { x1: "12", y1: "15", x2: "12", y2: "3" }]
]
];
export default Download;

27
tests/icons/globe.js Normal file
View File

@@ -0,0 +1,27 @@
const Globe = [
"svg",
{
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"
},
[
["circle", { cx: "12", cy: "12", r: "10" }],
["line", { x1: "2", y1: "12", x2: "22", y2: "12" }],
[
"path",
{
d:
"M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
}
]
]
];
export default Globe;

6
tests/icons/index.js Normal file
View File

@@ -0,0 +1,6 @@
export { default as Download } from './download';
export { default as Globe } from './globe';
export { default as Menu } from './menu';
export { default as Moon } from './moon';
export { default as Volume2 } from './volume-2';

21
tests/icons/menu.js Normal file
View File

@@ -0,0 +1,21 @@
const Menu = [
"svg",
{
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"
},
[
["line", { x1: "3", y1: "12", x2: "21", y2: "12" }],
["line", { x1: "3", y1: "6", x2: "21", y2: "6" }],
["line", { x1: "3", y1: "18", x2: "21", y2: "18" }]
]
];
export default Menu;

17
tests/icons/moon.js Normal file
View File

@@ -0,0 +1,17 @@
const Moon = [
"svg",
{
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"
},
[["path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" }]]
];
export default Moon;

23
tests/icons/volume-2.js Normal file
View File

@@ -0,0 +1,23 @@
const Volume2 = [
"svg",
{
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"
},
[
["polygon", { points: "11 5 6 9 2 9 2 15 6 15 11 19 11 5" }],
[
"path",
{ d: "M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07" }
]
]
];
export default Volume2;

59
tests/lucide.spec.js Normal file
View File

@@ -0,0 +1,59 @@
import * as icons from './icons';
import { createIcons } from '../src/lucide';
import fs from 'fs';
import path from 'path';
import { minify } from 'html-minifier';
const ICONS_DIR = path.resolve(__dirname, '../icons');
const getOriginalSvg = (iconName) => {
const svgContent = fs.readFileSync(path.join(ICONS_DIR, `${iconName}.svg`), 'utf8');
return minify(svgContent, { collapseWhitespace: true, keepClosingSlash: true, });
};
describe('createIcons', () => {
it('should read elements from DOM and replace it with icons', () => {
document.body.innerHTML = `<i icon-name="volume-2"></i>`;
createIcons({icons});
const svg = getOriginalSvg('volume-2');
expect(document.body.innerHTML).toMatchSnapshot()
});
it('should customize the name attribute', () => {
document.body.innerHTML = `<i custom-name="volume-2"></i>`;
createIcons({
icons,
nameAttr: 'custom-name'
});
const hasSvg = !!document.querySelector('svg');
expect(hasSvg).toBeTruthy()
});
it('should add custom attributes', () => {
document.body.innerHTML = `<i icon-name="volume-2"></i>`;
const attrs = {
class: 'icon custom-class',
fill: 'black',
};
createIcons({ icons, attrs });
const element = document.querySelector('svg');
const attributes = element.getAttributeNames();
const attributesAndValues = attributes.reduce((acc, item) => {
acc[item] = element.getAttribute(item);
return acc;
},{})
expect(attributesAndValues).toEqual(expect.objectContaining(attrs));
});
});

View File

@@ -0,0 +1,62 @@
import { getAttrs, getClassNames, combineClassNames } from '../src/replaceElement';
describe('getAtts', () => {
it('should returns attrbrutes of an element', () => {
const element = {
attributes: [
{
name: 'class',
value: 'item1 item2 item4',
},
{
name: 'date-name',
value: 'volume',
},
],
};
const attrs = getAttrs(element);
expect(attrs.class).toBe(element.attributes[0].value);
});
});
describe('getClassNames', () => {
it('should returns an array when giving class property of string', () => {
const elementAttrs = {
class: 'item1 item2 item3'
};
const attrs = getClassNames(elementAttrs);
expect(JSON.stringify(attrs)).toBe(JSON.stringify(['item1','item2','item3']));
});
it('should returns an array when givind class property with an array', () => {
const elementAttrs = {
class: ['item1','item2','item3']
};
const attrs = getClassNames(elementAttrs);
expect(JSON.stringify(attrs)).toBe(JSON.stringify(['item1','item2','item3']));
});
});
describe('combineClassNames', () => {
it('should retuns a string of classNames', () => {
const arrayOfClassnames = [
{
class: ['item1','item2','item3']
},
{
class: ['item4','item5','item6']
},
{
class: ['item7','item8','item9']
}
];
const combinedClassNames = combineClassNames(arrayOfClassnames);
expect(combinedClassNames).toMatchSnapshot();
});
});

7739
yarn.lock

File diff suppressed because it is too large Load Diff