mirror of
https://github.com/lucide-icons/lucide.git
synced 2025-12-14 20:27:42 +01:00
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:
11
.editorconfig
Normal file
11
.editorconfig
Normal 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
5
.eslintignore
Normal file
@@ -0,0 +1,5 @@
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
lib
|
||||
tests
|
||||
21
.eslintrc.json
Normal file
21
.eslintrc.json
Normal 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
8
.gitignore
vendored
@@ -2,3 +2,11 @@
|
||||
.next
|
||||
.now
|
||||
node_modules
|
||||
dist
|
||||
build
|
||||
lib
|
||||
sandbox
|
||||
stash
|
||||
coverage
|
||||
stats
|
||||
*.log
|
||||
|
||||
4
.npmignore
Normal file
4
.npmignore
Normal file
@@ -0,0 +1,4 @@
|
||||
.github
|
||||
packages
|
||||
stats
|
||||
build
|
||||
@@ -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.
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -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
132
README.md
@@ -1,10 +1,10 @@
|
||||
# Featherity
|
||||
# Lucide
|
||||
|
||||
[](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
27
babel.config.js
Normal 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
14
jest.config.js
Normal 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',
|
||||
},
|
||||
};
|
||||
67
package.json
67
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
77
rollup.config.js
Normal 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;
|
||||
23
scripts/build/generateExportsFile.js
Normal file
23
scripts/build/generateExportsFile.js
Normal 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`);
|
||||
}
|
||||
27
scripts/build/generateIconFiles.js
Normal file
27
scripts/build/generateIconFiles.js
Normal 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
42
scripts/buildIcons.js
Normal 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
74
scripts/helpers.js
Normal 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
15
scripts/optimizeSvgs.js
Normal 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));
|
||||
});
|
||||
11
scripts/render/default-attrs.json
Normal file
11
scripts/render/default-attrs.json
Normal 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"
|
||||
}
|
||||
57
scripts/render/processSvg.js
Normal file
57
scripts/render/processSvg.js
Normal 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;
|
||||
29
scripts/render/renderIconNodes.js
Normal file
29
scripts/render/renderIconNodes.js
Normal 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;
|
||||
};
|
||||
35
scripts/render/renderIconsObject.js
Normal file
35
scripts/render/renderIconsObject.js
Normal 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
19
src/createElement.js
Normal 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
5
src/icons/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/*
|
||||
Icons exports.
|
||||
|
||||
Will be generated
|
||||
*/
|
||||
34
src/lucide.js
Normal file
34
src/lucide.js
Normal 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
83
src/replaceElement.js
Normal 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);
|
||||
};
|
||||
3
tests/__snapshots__/lucide.spec.js.snap
Normal file
3
tests/__snapshots__/lucide.spec.js.snap
Normal 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>"`;
|
||||
3
tests/__snapshots__/replaceElement.spec.js.snap
Normal file
3
tests/__snapshots__/replaceElement.spec.js.snap
Normal 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
21
tests/icons/download.js
Normal 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
27
tests/icons/globe.js
Normal 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
6
tests/icons/index.js
Normal 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
21
tests/icons/menu.js
Normal 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
17
tests/icons/moon.js
Normal 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
23
tests/icons/volume-2.js
Normal 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
59
tests/lucide.spec.js
Normal 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));
|
||||
});
|
||||
});
|
||||
62
tests/replaceElement.spec.js
Normal file
62
tests/replaceElement.spec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user