This commit is contained in:
John Letey
2020-06-08 16:39:52 +01:00
commit 433bbae4f1
337 changed files with 21687 additions and 0 deletions

6
bin/.eslintrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"rules": {
"import/no-extraneous-dependencies": "off",
"no-console": "off"
}
}

View File

@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`builds object correctly 1`] = `
Object {
"icon1": "<line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\"></line><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\"></line>",
"icon2": "<circle cx=\\"12\\" cy=\\"12\\" r=\\"11\\"></circle>",
}
`;

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`builds sprite correctly 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\"><defs><symbol id=\\"icon1\\" viewBox=\\"0 0 24 24\\"><line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\"></line><line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\"></line></symbol><symbol id=\\"icon2\\" viewBox=\\"0 0 24 24\\"><circle cx=\\"12\\" cy=\\"12\\" r=\\"11\\"></circle></symbol></defs></svg>"`;

View File

@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`processes SVG correctly 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\\"
>
<line x1=\\"23\\" y1=\\"1\\" x2=\\"1\\" y2=\\"23\\" />
<line x1=\\"1\\" y1=\\"1\\" x2=\\"23\\" y2=\\"23\\" />
</svg>
"
`;
exports[`rejects when passed unparsable SVG string 1`] = `
[Error: Error in parsing SVG: Unclosed root tag
Line: 0
Column: 10
Char: ]
`;

View File

@@ -0,0 +1,17 @@
/* eslint-env jest */
import buildIconsObject from '../build-icons-object';
const SVG_FILES = {
'icon1.svg':
'<svg\n xmlns="http://www.w3.org/2000/svg"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n>\n <line x1="23" y1="1" x2="1" y2="23" />\n <line x1="1" y1="1" x2="23" y2="23" />\n</svg>',
'icon2.svg':
'<svg\n xmlns="http://www.w3.org/2000/svg"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n>\n <circle cx="12" cy="12" r="11" />\n</svg>',
};
function getSvg(svgFile) {
return SVG_FILES[svgFile];
}
test('builds object correctly', () => {
expect(buildIconsObject(Object.keys(SVG_FILES), getSvg)).toMatchSnapshot();
});

View File

@@ -0,0 +1,12 @@
/* eslint-env jest */
import buildSpriteString from '../build-sprite-string';
const icons = {
icon1:
'<line x1="23" y1="1" x2="1" y2="23"></line><line x1="1" y1="1" x2="23" y2="23"></line>',
icon2: '<circle cx="12" cy="12" r="11"></circle>',
};
test('builds sprite correctly', () => {
expect(buildSpriteString(icons)).toMatchSnapshot();
});

View File

@@ -0,0 +1,15 @@
/* eslint-env jest */
import processSvg from '../process-svg';
test('processes SVG correctly', () => {
const SVG =
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>Title</title><line x1="23" y1="1" x2="1" y2="23" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><line x1="1" y1="1" x2="23" y2="23" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/></svg>';
expect(processSvg(SVG)).resolves.toMatchSnapshot();
});
test('rejects when passed unparsable SVG string', () => {
const UNPARSABLE_SVG = '<svg></svg';
expect(processSvg(UNPARSABLE_SVG)).rejects.toMatchSnapshot();
});

19
bin/build-icons-json.js Normal file
View File

@@ -0,0 +1,19 @@
import fs from 'fs';
import path from 'path';
import buildIconsObject from './build-icons-object';
const IN_DIR = path.resolve(__dirname, '../icons');
const OUT_FILE = path.resolve(__dirname, '../dist/icons.json');
console.log(`Building ${OUT_FILE}...`);
const svgFiles = fs
.readdirSync(IN_DIR)
.filter(file => path.extname(file) === '.svg');
const getSvg = svgFile => fs.readFileSync(path.join(IN_DIR, svgFile));
const icons = buildIconsObject(svgFiles, getSvg);
fs.writeFileSync(OUT_FILE, JSON.stringify(icons));

35
bin/build-icons-object.js Normal file
View File

@@ -0,0 +1,35 @@
import path from 'path';
import cheerio from 'cheerio';
import { minify } from 'html-minifier';
/**
* 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}
*/
function buildIconsObject(svgFiles, getSvg) {
return svgFiles
.map(svgFile => {
const name = path.basename(svgFile, '.svg');
const svg = getSvg(svgFile);
const contents = getSvgContents(svg);
return { name, contents };
})
.reduce((icons, icon) => {
icons[icon.name] = icon.contents;
return icons;
}, {});
}
/**
* 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 });
}
export default buildIconsObject;

View File

@@ -0,0 +1,28 @@
import DEFAULT_ATTRS from '../src/default-attrs.json';
/**
* Build an SVG sprite string containing SVG symbols.
* @param {Object} icons
* @returns {string}
*/
function buildSpriteString(icons) {
const symbols = Object.keys(icons)
.map(icon => toSvgSymbol(icon, icons[icon]))
.join('');
return `<svg xmlns="${DEFAULT_ATTRS.xmlns}"><defs>${symbols}</defs></svg>`;
}
/**
* Create an SVG symbol string.
* @param {string} name - Icon name
* @param {string} contents - SVG contents
* @returns {string}
*/
function toSvgSymbol(name, contents) {
return `<symbol id="${name}" viewBox="${DEFAULT_ATTRS.viewBox}">${
contents
}</symbol>`;
}
export default buildSpriteString;

10
bin/build-sprite.js Normal file
View File

@@ -0,0 +1,10 @@
import fs from 'fs';
import path from 'path';
import icons from '../dist/icons.json';
import buildSpriteString from './build-sprite-string';
const OUT_FILE = path.resolve(__dirname, '../dist/feather-sprite.svg');
console.log(`Building ${OUT_FILE}...`);
fs.writeFileSync(OUT_FILE, buildSpriteString(icons));

13
bin/build-svgs.js Normal file
View File

@@ -0,0 +1,13 @@
import fs from 'fs';
import path from 'path';
import icons from '../src/icons';
const OUT_DIR = path.resolve(__dirname, '../dist/icons');
console.log(`Building SVGs in ${OUT_DIR}...`);
Object.keys(icons).forEach(name => {
const svg = icons[name].toSvg();
fs.writeFileSync(path.join(OUT_DIR, `${name}.svg`), svg);
});

25
bin/build.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
# Process SVG files
npx babel-node bin/process-svgs.js
# Create dist directory
npx rimraf dist
mkdir dist
# Build icons.json
npx babel-node bin/build-icons-json.js
# Build SVG sprite
npx babel-node bin/build-sprite.js
# Create dist/icons directory
npx rimraf dist/icons
mkdir dist/icons
# Build SVG icons
npx babel-node bin/build-svgs.js
# Build JavaScript library
npx webpack --output-filename feather.js --mode development
npx webpack --output-filename feather.min.js --mode production

58
bin/process-svg.js Normal file
View File

@@ -0,0 +1,58 @@
import Svgo from 'svgo';
import cheerio from 'cheerio';
import { format } from 'prettier';
import DEFAULT_ATTRS from '../src/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;

18
bin/process-svgs.js Normal file
View File

@@ -0,0 +1,18 @@
import fs from 'fs';
import path from 'path';
import processSvg from './process-svg';
const IN_DIR = path.resolve(__dirname, '../icons');
console.log(`Processing SVGs in ${IN_DIR}...`);
fs
.readdirSync(IN_DIR)
.filter(file => path.extname(file) === '.svg')
.forEach(svgFile => {
const svg = fs.readFileSync(path.join(IN_DIR, svgFile));
processSvg(svg).then(svg =>
fs.writeFileSync(path.join(IN_DIR, svgFile), svg),
);
});

73
bin/sync-algolia.js Normal file
View File

@@ -0,0 +1,73 @@
import algolia from 'algoliasearch';
import icons from '../dist/icons.json';
import tags from '../src/tags.json';
const ALGOLIA_APP_ID = '5EEOG744D0';
if (
process.env.TRAVIS_PULL_REQUEST === 'false' &&
process.env.TRAVIS_BRANCH === 'master'
) {
syncAlgolia();
} else {
console.log('Skipped Algolia sync.');
}
function syncAlgolia() {
// ALGOLIA_ADMIN_KEY must be added as an environment variable in Travis CI
const client = algolia(ALGOLIA_APP_ID, process.env.ALGOLIA_ADMIN_KEY);
console.log('Initializing target and temporary indexes...');
const index = client.initIndex('icons');
const indexTmp = client.initIndex('icons_tmp');
console.log(
"Copying target index's settings, synonyms and rules into temporary index...",
);
scopedCopyIndex(client, index.indexName, indexTmp.indexName)
.then(() => {
const objects = Object.keys(icons).map(name => ({
name,
tags: tags[name] || [],
}));
console.log('Adding objects to the temporary index...');
return addObjects(indexTmp, objects);
})
.then(() => {
console.log('Moving temporary index to target index...');
return moveIndex(client, indexTmp.indexName, index.indexName);
});
}
function scopedCopyIndex(
client,
indexNameSrc,
indexNameDest,
scope = ['settings', 'synonyms', 'rules'],
) {
return new Promise((resolve, reject) => {
client.copyIndex(indexNameSrc, indexNameDest, scope, (error, contents) => {
if (error) reject(error);
resolve(contents);
});
});
}
function addObjects(index, objects) {
return new Promise((resolve, reject) => {
index.addObjects(objects, (error, contents) => {
if (error) reject(error);
resolve(contents);
});
});
}
function moveIndex(client, indexNameSrc, indexNameDest) {
return new Promise((resolve, reject) => {
client.moveIndex(indexNameSrc, indexNameDest, (error, contents) => {
if (error) reject(error);
resolve(contents);
});
});
}