Convert simple-icons manual backup from submodule to tracked files
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Script to add data for a new icon to the simple-icons dataset.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import("../sdk.js").IconData} IconData
|
||||
*/
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import {checkbox, confirm, input, search} from '@inquirer/prompts';
|
||||
import chalk from 'chalk';
|
||||
import {search as fuzzySearch} from 'fast-fuzzy';
|
||||
import getRelativeLuminance from 'get-relative-luminance';
|
||||
import {getIconsDataString, normalizeColor, titleToSlug} from '../sdk.mjs';
|
||||
import {
|
||||
formatIconData,
|
||||
getJsonSchemaData,
|
||||
getSpdxLicenseIds,
|
||||
sortIconsCompare,
|
||||
writeIconsData,
|
||||
} from './utils.js';
|
||||
|
||||
process.exitCode = 1;
|
||||
process.on('uncaughtException', (error) => {
|
||||
if (error instanceof Error && error.name === 'ExitPromptError') {
|
||||
process.stdout.write('\nAborted\n');
|
||||
process.exit(1);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
/** @type {import('../types.d.ts').IconData[]} */
|
||||
const iconsData = JSON.parse(await getIconsDataString());
|
||||
const jsonSchema = await getJsonSchemaData();
|
||||
|
||||
const HEX_REGEX = /^#?[a-f\d]{3,8}$/i;
|
||||
|
||||
const aliasTypes = ['aka', 'old'].map((key) => ({
|
||||
name: `${key} (${jsonSchema.definitions.brand.properties.aliases.properties[key].description})`,
|
||||
value: key,
|
||||
}));
|
||||
|
||||
const spdxLicenseIds = await getSpdxLicenseIds();
|
||||
const licenseTypes = [
|
||||
{name: 'Custom', value: 'custom'},
|
||||
...spdxLicenseIds.map((id) => ({name: id, value: id})),
|
||||
];
|
||||
|
||||
/**
|
||||
* Build a regex to validate HTTPs URLs.
|
||||
* @returns {Promise<RegExp>} Regex to validate HTTPs URLs.
|
||||
*/
|
||||
const urlRegex = async () =>
|
||||
new RegExp(
|
||||
JSON.parse(
|
||||
await fs.readFile(
|
||||
path.resolve(import.meta.dirname, '..', '.jsonschema.json'),
|
||||
'utf8',
|
||||
),
|
||||
).definitions.url.pattern,
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether an input is a valid URL.
|
||||
* @param {string} input URL input.
|
||||
* @returns {Promise<boolean|string>} Whether the input is a valid URL.
|
||||
*/
|
||||
const isValidURL = async (input) => {
|
||||
const regex = await urlRegex();
|
||||
return regex.test(input) || 'Must be a valid and secure (https://) URL.';
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether an input is a valid hex color.
|
||||
* @param {string} input Hex color.
|
||||
* @returns {boolean|string} Whether the input is a valid hex color.
|
||||
*/
|
||||
const isValidHexColor = (input) =>
|
||||
HEX_REGEX.test(input) || 'Must be a valid hex code.';
|
||||
|
||||
/**
|
||||
* Whether an icon is not already in the dataset.
|
||||
* @param {string} input New icon input.
|
||||
* @returns {boolean} Whether the icon is new.
|
||||
*/
|
||||
const isNewIcon = (input) =>
|
||||
!iconsData.some(
|
||||
(icon) =>
|
||||
icon.title === input || titleToSlug(icon.title) === titleToSlug(input),
|
||||
);
|
||||
|
||||
/**
|
||||
* Compute a preview of a color to use in prompt background.
|
||||
* @param {string} input Color input.
|
||||
* @returns {string} Preview of the color.
|
||||
*/
|
||||
const previewHexColor = (input) => {
|
||||
const color = normalizeColor(input);
|
||||
const luminance = HEX_REGEX.test(input)
|
||||
? getRelativeLuminance.default(`#${color}`)
|
||||
: -1;
|
||||
if (luminance === -1) return input.toUpperCase();
|
||||
return chalk.bgHex(`#${color}`).hex(luminance < 0.4 ? '#fff' : '#000')(
|
||||
input.toUpperCase(),
|
||||
);
|
||||
};
|
||||
|
||||
/** @type {IconData} */
|
||||
// @ts-expect-error: `slug` is not required in our source simple-icons.json file.
|
||||
const answers = {
|
||||
title: '',
|
||||
hex: '',
|
||||
source: '',
|
||||
};
|
||||
|
||||
answers.title = await input({
|
||||
message: 'What is the title of this icon?',
|
||||
validate: (input) =>
|
||||
input.trim().length > 0
|
||||
? isNewIcon(input) || 'This icon title or slug already exists.'
|
||||
: 'This field is required.',
|
||||
});
|
||||
|
||||
answers.hex = normalizeColor(
|
||||
await input({
|
||||
message: 'What is the brand color of this icon?',
|
||||
validate: isValidHexColor,
|
||||
transformer: previewHexColor,
|
||||
}),
|
||||
);
|
||||
|
||||
answers.source = await input({
|
||||
message: 'What is the source URL of the icon?',
|
||||
validate: isValidURL,
|
||||
});
|
||||
|
||||
if (
|
||||
await confirm({
|
||||
message: 'Does this icon have brand guidelines?',
|
||||
})
|
||||
) {
|
||||
answers.guidelines = await input({
|
||||
message: 'What is the URL for the brand guidelines?',
|
||||
validate: isValidURL,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
await confirm({
|
||||
message: 'Does this icon have a license?',
|
||||
})
|
||||
) {
|
||||
answers.license = {
|
||||
type: await search({
|
||||
message: "What is the icon's license?",
|
||||
async source(input) {
|
||||
input = (input || '').trim();
|
||||
return input
|
||||
? fuzzySearch(input, licenseTypes, {
|
||||
keySelector: (x) => x.value,
|
||||
})
|
||||
: licenseTypes;
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
if (answers.license.type === 'custom') {
|
||||
// @ts-expect-error
|
||||
answers.license.url = await input({
|
||||
message: `What is the URL for the license? (optional)`,
|
||||
validate: (input) => input.length === 0 || isValidURL(input),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
await confirm({
|
||||
message: 'Does this icon have brand aliases?',
|
||||
default: false,
|
||||
})
|
||||
) {
|
||||
answers.aliases = await checkbox({
|
||||
message: 'What types of aliases do you want to add?',
|
||||
choices: aliasTypes,
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
.then(async (aliases) => {
|
||||
/** @type {{[_: string]: string[]}} */
|
||||
const result = {};
|
||||
for (const alias of aliases) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
result[alias] = await input({
|
||||
message: `What ${alias} aliases would you like to add? (separate with commas)`,
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
.then((aliases) => aliases.split(',').map((alias) => alias.trim()));
|
||||
}
|
||||
|
||||
return aliases.length > 0 ? result : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
'About to write the following to simple-icons.json:\n' +
|
||||
JSON.stringify(answers, null, '\t') +
|
||||
'\n',
|
||||
);
|
||||
|
||||
if (
|
||||
await confirm({
|
||||
message: 'Is this OK?',
|
||||
})
|
||||
) {
|
||||
iconsData.push(answers);
|
||||
iconsData.sort(sortIconsCompare);
|
||||
await writeIconsData(formatIconData(iconsData));
|
||||
process.stdout.write(chalk.green('\nData written successfully.\n'));
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.stdout.write(chalk.red('\nAborted.\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/* eslint jsdoc/reject-any-type: off */
|
||||
/**
|
||||
* @file Auto-close script for closing won't add icons.
|
||||
*/
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
/**
|
||||
* @typedef {object} Rule
|
||||
* @property {RegExp[]} patterns - The pattern to match against the issue title.
|
||||
* @property {string} reason - The issue numbers to include in the reason text.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Rule[]} Config
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Issue
|
||||
* @property {{name: string}[]} labels - Issue labels.
|
||||
* @property {string} state - Issue state, possible values are 'open' and 'closed'.
|
||||
* @property {string} title - Issue title.
|
||||
* @property {string} body - Issue body.
|
||||
*/
|
||||
|
||||
/** @type {Config} */
|
||||
const rules = await import(
|
||||
path.join(import.meta.dirname, 'autoclose.rules.js')
|
||||
).then((module) => module.default);
|
||||
|
||||
const {GITHUB_TOKEN, GITHUB_REPOSITORY, ISSUE_NUMBER} = process.env;
|
||||
|
||||
if ([GITHUB_TOKEN, GITHUB_REPOSITORY, ISSUE_NUMBER].some((v) => !v)) {
|
||||
console.error(
|
||||
`${Object.entries({GITHUB_TOKEN, GITHUB_REPOSITORY, ISSUE_NUMBER})
|
||||
.filter(([, v]) => !v)
|
||||
.map(([k]) => k)
|
||||
.join(', ')} environment variable(s) must be set.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data from GitHub API.
|
||||
* @param {string} url The URL to fetch.
|
||||
* @param {globalThis.RequestInit} options The options to pass to the fetch function.
|
||||
* @returns {Promise<any>} - The response data.
|
||||
*/
|
||||
const githubFetch = async (url, options) => {
|
||||
const response = await globalThis.fetch(new URL(url), {
|
||||
...options,
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
Authorization: `Bearer ${GITHUB_TOKEN}`,
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch ${url}: ${response.status} (${response.statusText}).`,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the issue is a won't add icon issue.
|
||||
* GitHub REST API: https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue.
|
||||
* @returns {Promise<string | undefined>} - Returns reason if the issue is a won't add icon issue, undefined otherwise.
|
||||
*/
|
||||
const checkIfCanBeClosed = async () => {
|
||||
const url = `https://api.github.com/repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}`;
|
||||
const response = await githubFetch(url, {method: 'GET'});
|
||||
|
||||
/** @type {Issue} */
|
||||
const json = await response.json();
|
||||
const {labels, state, title, body} = json;
|
||||
const labelNames = new Set(labels.map((label) => label.name));
|
||||
if (state === 'closed') return undefined;
|
||||
if (labelNames.has('meta')) return undefined;
|
||||
if (labelNames.has('in discussion')) return undefined;
|
||||
if (!labelNames.has('new icon')) return undefined;
|
||||
const matched = rules.find((rule) =>
|
||||
rule.patterns.some((pattern) => {
|
||||
const brandNamePattern = new RegExp(
|
||||
`### Brand Name\n*${pattern.source.replaceAll('$', '')}\n*###`,
|
||||
'i',
|
||||
);
|
||||
return pattern.test(title) || brandNamePattern.test(body);
|
||||
}),
|
||||
);
|
||||
if (!matched) return undefined;
|
||||
return matched.reason;
|
||||
};
|
||||
|
||||
/**
|
||||
* Close the issue as not planned.
|
||||
* GitHub REST API: https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#update-an-issue.
|
||||
*/
|
||||
const closeAsNotPlanned = async () => {
|
||||
const url = `https://api.github.com/repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}`;
|
||||
await githubFetch(url, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned', // eslint-disable-line camelcase
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add labels to the issue.
|
||||
* GitHub REST API: https://docs.github.com/en/rest/issues/labels?apiVersion=2022-11-28#add-labels-to-an-issue.
|
||||
*/
|
||||
const addLabels = async () => {
|
||||
const url = `https://api.github.com/repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}/labels`;
|
||||
await githubFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
labels: ['duplicate', "won't add"],
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Post a comment on the issue.
|
||||
* GitHub REST API: https://docs.github.com/en/rest/issues/comments?apiVersion=2022-11-28#create-an-issue-comment.
|
||||
* @param {string} reason The reason for closing the issue.
|
||||
*/
|
||||
const commentWithReason = async (reason) => {
|
||||
const url = `https://api.github.com/repos/${GITHUB_REPOSITORY}/issues/${ISSUE_NUMBER}/comments`;
|
||||
await githubFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
body: reason,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const reason = await checkIfCanBeClosed();
|
||||
if (reason) {
|
||||
await closeAsNotPlanned();
|
||||
await addLabels();
|
||||
await commentWithReason(reason);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
} else {
|
||||
console.error(String(error));
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* @file Auto-close rules for closing won't add icons.
|
||||
*/
|
||||
|
||||
const autocloseTerm = 'This issue was automatically closed. Please refer to ';
|
||||
|
||||
/** @type {import('./autoclose.app.js').Config} */
|
||||
const rules = [
|
||||
{
|
||||
patterns: [/matlab/i],
|
||||
reason: autocloseTerm + '#1233.',
|
||||
},
|
||||
{
|
||||
patterns: [/disney\s*(?:plus|\+)?/i],
|
||||
reason: autocloseTerm + '#2309.',
|
||||
},
|
||||
{
|
||||
patterns: [/british\s*petroleum/i],
|
||||
reason: autocloseTerm + '#5849.',
|
||||
},
|
||||
{
|
||||
patterns: [/mattel/i, /barbie/i, /hot\s*wheels/i, /fisher[-\s]*price/i],
|
||||
reason: autocloseTerm + '#6656.',
|
||||
},
|
||||
{
|
||||
patterns: [/oracle/i, /java(?:\s|$)/i],
|
||||
reason: autocloseTerm + '#7374.',
|
||||
},
|
||||
{
|
||||
patterns: [/microchip/i],
|
||||
reason: autocloseTerm + '#9373.',
|
||||
},
|
||||
{
|
||||
patterns: [/adobe/i, /photoshop/i],
|
||||
reason: autocloseTerm + '#10018.',
|
||||
},
|
||||
{
|
||||
patterns: [/microsoft/i, /vs\s*code/, /visual\s*studio/i, /windows/i],
|
||||
reason: autocloseTerm + '#11236.',
|
||||
},
|
||||
{
|
||||
patterns: [/linked\s*in/i],
|
||||
reason: autocloseTerm + '#11236 #11372.',
|
||||
},
|
||||
{
|
||||
patterns: [/amazon/i, /aws/i],
|
||||
reason: autocloseTerm + '#13056.',
|
||||
},
|
||||
{
|
||||
patterns: [/yahoo/i, /engadget/i, /aol/i],
|
||||
reason: autocloseTerm + '#9861.',
|
||||
},
|
||||
];
|
||||
|
||||
export default rules;
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Clean files built by the build process.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import {fileExists} from '../utils.js';
|
||||
|
||||
const files = [
|
||||
'index.js',
|
||||
'index-icons.js',
|
||||
'index.mjs',
|
||||
'index-icons.mjs',
|
||||
'index.d.ts',
|
||||
'sdk.js',
|
||||
];
|
||||
|
||||
try {
|
||||
Promise.all(
|
||||
files.map(async (file) => {
|
||||
const filepath = path.resolve(import.meta.dirname, '..', '..', file);
|
||||
if (!(await fileExists(filepath))) {
|
||||
console.error(`File ${file} does not exist, skipping...`);
|
||||
return;
|
||||
}
|
||||
|
||||
return fs.unlink(filepath);
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error cleaning files:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Simple Icons package build script.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../types.js').License} License
|
||||
*/
|
||||
|
||||
import {promises as fs} from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {format} from 'node:util';
|
||||
import {transform as esbuildTransform} from 'esbuild';
|
||||
import {
|
||||
getIconSlug,
|
||||
getIconsData,
|
||||
slugToVariableName,
|
||||
svgToPath,
|
||||
titleToHtmlFriendly,
|
||||
} from '../../sdk.mjs';
|
||||
import {sortIconsCompare} from '../utils.js';
|
||||
|
||||
const UTF8 = 'utf8';
|
||||
|
||||
const rootDirectory = path.resolve(import.meta.dirname, '..', '..');
|
||||
const iconsDirectory = path.resolve(rootDirectory, 'icons');
|
||||
const indexJsFile = path.resolve(rootDirectory, 'index.js');
|
||||
const indexMjsFile = path.resolve(rootDirectory, 'index.mjs');
|
||||
const sdkJsFile = path.resolve(rootDirectory, 'sdk.js');
|
||||
const sdkMjsFile = path.resolve(rootDirectory, 'sdk.mjs');
|
||||
const indexDtsFile = path.resolve(rootDirectory, 'index.d.ts');
|
||||
|
||||
const templatesDirectory = path.resolve(import.meta.dirname, 'templates');
|
||||
const iconObjectTemplateFile = path.resolve(
|
||||
templatesDirectory,
|
||||
'icon-object.js.template',
|
||||
);
|
||||
|
||||
/**
|
||||
* Merged type from icon data and icon JS object needed to build by reference
|
||||
* to not decrease performance in the build process.
|
||||
* @typedef {import('../../types.js').SimpleIcon & import('../../types.d.ts').IconData} IconDataAndObject
|
||||
*/
|
||||
|
||||
const icons = await getIconsData();
|
||||
const iconObjectTemplate = await fs.readFile(iconObjectTemplateFile, UTF8);
|
||||
|
||||
/**
|
||||
* Escape a string for use in a JavaScript string.
|
||||
* @param {string} value The value to escape.
|
||||
* @returns {string} The escaped value.
|
||||
*/
|
||||
const escape = (value) => value.replaceAll(/(?<!\\)'/g, String.raw`\'`);
|
||||
|
||||
/**
|
||||
* Converts a license object to a URL if the URL is not defined.
|
||||
* @param {License} license The license object or URL.
|
||||
* @returns {License} The license object with a URL.
|
||||
*/
|
||||
const licenseToString = (license) => {
|
||||
if (license.url === undefined) {
|
||||
license.url = `https://spdx.org/licenses/${license.type}`;
|
||||
}
|
||||
|
||||
return license;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an icon object to a JavaScript object.
|
||||
* @param {IconDataAndObject} icon The icon object.
|
||||
* @returns {string} The JavaScript object.
|
||||
*/
|
||||
const iconDataAndObjectToJsRepr = (icon) =>
|
||||
format(
|
||||
iconObjectTemplate,
|
||||
escape(icon.title),
|
||||
escape(icon.slug),
|
||||
escape(titleToHtmlFriendly(icon.title)),
|
||||
escape(icon.path),
|
||||
escape(icon.source),
|
||||
escape(icon.hex),
|
||||
icon.guidelines ? `\n guidelines: '${escape(icon.guidelines)}',` : '',
|
||||
icon.license === undefined
|
||||
? ''
|
||||
: `\n license: ${JSON.stringify(licenseToString(icon.license))},`,
|
||||
);
|
||||
|
||||
/**
|
||||
* Write JavaScript content to a file.
|
||||
*
|
||||
* ESBuild by default uses `ascii` encoding for JavaScript files, so the titles of icons
|
||||
* are encoded using escape sequences (eg. "Aerom\xE9xico" instead of "Aeroméxico").
|
||||
* See {@link https://esbuild.github.io/api/#charset}.
|
||||
* Although this adds a minimal size overhead, it is needed to ensure that our distributed
|
||||
* JavaScript files are compatible with all JavaScript environments. Especially, browsers
|
||||
* that are not using `<meta charset="utf-8">` in their HTML. As we support browsers
|
||||
* without meta charset in SVG `<title>` elements, we need to ensure the same for scripts.
|
||||
* @param {string} filepath The path to the file to write.
|
||||
* @param {string} rawJavaScript The raw JavaScript content to write to the file.
|
||||
* @param {'cjs'} [format] The format of the resulting JavaScript file.
|
||||
*/
|
||||
const writeJs = async (filepath, rawJavaScript, format = undefined) => {
|
||||
/** @type {import('esbuild').TransformOptions} */
|
||||
const options = {minify: true, charset: 'ascii', format};
|
||||
const {code} = await esbuildTransform(rawJavaScript, options);
|
||||
// ESBuild adds a trailing newline to the end of the file
|
||||
await fs.writeFile(filepath, code.trimEnd());
|
||||
};
|
||||
|
||||
/**
|
||||
* Write TypeScript content to a file.
|
||||
* @param {string} filepath The path to the file to write.
|
||||
* @param {string} rawTypeScript The raw TypeScript content to write to the file.
|
||||
*/
|
||||
const writeTs = async (filepath, rawTypeScript) => {
|
||||
await fs.writeFile(filepath, rawTypeScript);
|
||||
};
|
||||
|
||||
/**
|
||||
* Build icons intermediate instances.
|
||||
* @returns {Promise<{
|
||||
* icon: IconDataAndObject,
|
||||
* iconObjectRepr: string,
|
||||
* iconExportName: string
|
||||
* }[]>} Merged icon data and object instances.
|
||||
*/
|
||||
const buildIcons = async () =>
|
||||
Promise.all(
|
||||
icons.map(async (iconData) => {
|
||||
const slug = getIconSlug(iconData);
|
||||
const svgFilepath = path.resolve(iconsDirectory, `${slug}.svg`);
|
||||
const svg = await fs.readFile(svgFilepath, UTF8);
|
||||
/** @type {IconDataAndObject} */
|
||||
const icon = {};
|
||||
Object.assign(icon, iconData);
|
||||
icon.svg = svg;
|
||||
icon.path = svgToPath(svg);
|
||||
icon.slug = slug;
|
||||
const iconObjectRepr = iconDataAndObjectToJsRepr(icon);
|
||||
const iconExportName = slugToVariableName(slug);
|
||||
return {icon, iconObjectRepr, iconExportName};
|
||||
}),
|
||||
);
|
||||
|
||||
const build = async () => {
|
||||
const builtIcons = await buildIcons();
|
||||
|
||||
const iconsBarrelDts = [];
|
||||
const iconsBarrelJs = [];
|
||||
const iconsBarrelMjs = [];
|
||||
|
||||
builtIcons.sort((a, b) => sortIconsCompare(a.icon, b.icon));
|
||||
for (const {iconObjectRepr, iconExportName} of builtIcons) {
|
||||
iconsBarrelDts.push(`export const ${iconExportName}:I;`);
|
||||
iconsBarrelJs.push(`${iconExportName}:${iconObjectRepr},`);
|
||||
iconsBarrelMjs.push(`export const ${iconExportName}=${iconObjectRepr}`);
|
||||
}
|
||||
|
||||
// Constants used in templates to reduce package size
|
||||
const constantsString = `const a='<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>',b='</title><path d="',c='"/></svg>';`;
|
||||
|
||||
// Write our file containing the exports of all icons in CommonJS ...
|
||||
const rawIndexJs = `${constantsString}module.exports={${iconsBarrelJs.join(
|
||||
'',
|
||||
)}};`;
|
||||
await writeJs(indexJsFile, rawIndexJs);
|
||||
// ... and ESM
|
||||
const rawIndexMjs = constantsString + iconsBarrelMjs.join('');
|
||||
await writeJs(indexMjsFile, rawIndexMjs);
|
||||
// ... and create a type declaration file
|
||||
const rawIndexDts = `import {SimpleIcon} from "./types";export {SimpleIcon};type I=SimpleIcon;${iconsBarrelDts.join(
|
||||
'',
|
||||
)}`;
|
||||
await writeTs(indexDtsFile, rawIndexDts);
|
||||
|
||||
// Create a CommonJS SDK file
|
||||
await writeJs(sdkJsFile, await fs.readFile(sdkMjsFile, UTF8), 'cjs');
|
||||
|
||||
// Build deprecated `simple-icons/icons` entrypoint.
|
||||
// TODO: This must be removed at v17.
|
||||
const deprecatedMessage =
|
||||
`⚠️ The entrypoint 'simple-icons/icons' is deprecated and` +
|
||||
` will be removed in version 17.0.0`;
|
||||
const jsDeprecationMessage =
|
||||
`${deprecatedMessage}. Please, import icons from 'simple-icons'` +
|
||||
` using \`require('simple-icons')\` instead of \`require('simple-icons/icons')\`.`;
|
||||
const iconsIndexJs =
|
||||
`console.warn("${jsDeprecationMessage}");` +
|
||||
`module.exports=require('./index.js');`;
|
||||
const iconsIndexJsFile = path.resolve(rootDirectory, 'index-icons.js');
|
||||
await writeJs(iconsIndexJsFile, iconsIndexJs);
|
||||
|
||||
const mjsDeprecationMessage =
|
||||
`${deprecatedMessage}. Please, import icons from 'simple-icons'` +
|
||||
` using \`import ... from 'simple-icons'\` instead of \`import ... from 'simple-icons/icons'\`.`;
|
||||
const iconsIndexMjs =
|
||||
`console.warn("${mjsDeprecationMessage}");` +
|
||||
`export * from './index.mjs';`;
|
||||
const iconsIndexMjsFile = path.resolve(rootDirectory, 'index-icons.mjs');
|
||||
await writeJs(iconsIndexMjsFile, iconsIndexMjs);
|
||||
};
|
||||
|
||||
await build();
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
title: '%s',
|
||||
slug: '%s',
|
||||
get svg() {
|
||||
return a + '%s' + b + this.path + c;
|
||||
},
|
||||
path: '%s',
|
||||
source: '%s',
|
||||
hex: '%s',%s%s
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Format data/simple-icons.json.
|
||||
*/
|
||||
import {getIconsData} from '../sdk.mjs';
|
||||
import {formatIconData, writeIconsData} from './utils.js';
|
||||
|
||||
const icons = await getIconsData();
|
||||
writeIconsData(formatIconData(icons));
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Script that takes a brand name as argument and outputs the corresponding
|
||||
* icon SVG filename to standard output.
|
||||
*/
|
||||
|
||||
import process from 'node:process';
|
||||
import {titleToSlug} from '../sdk.mjs';
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
console.error('Provide a brand name as argument');
|
||||
process.exit(1);
|
||||
} else {
|
||||
const brandName = process.argv[2];
|
||||
const filename = titleToSlug(brandName);
|
||||
process.stdout.write(
|
||||
`For '${brandName}' use the file 'icons/${filename}.svg'\n`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* CLI tool to run jsonschema on the simple-icons.json data file.
|
||||
*/
|
||||
|
||||
import process from 'node:process';
|
||||
import {Validator} from 'jsonschema';
|
||||
import {getIconsData} from '../../sdk.mjs';
|
||||
import {getJsonSchemaData} from '../utils.js';
|
||||
|
||||
const icons = await getIconsData();
|
||||
const schema = await getJsonSchemaData();
|
||||
|
||||
const validator = new Validator();
|
||||
const result = validator.validate(icons, schema);
|
||||
if (result.errors.length > 0) {
|
||||
for (const error of result.errors) console.error(error);
|
||||
console.error(`Found ${result.errors.length} error(s) in simple-icons.json`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Linters for the package that can't easily be implemented in the existing ones.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import("../../types.js").IconData} IconData
|
||||
* @typedef {import("../../types.js").CustomLicense} CustomLicense
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import fakeDiff from 'fake-diff';
|
||||
import {
|
||||
collator,
|
||||
getIconSlug,
|
||||
getIconsDataString,
|
||||
normalizeNewlines,
|
||||
titleToSlug,
|
||||
} from '../../sdk.mjs';
|
||||
import {
|
||||
fileExists,
|
||||
formatIconData,
|
||||
getSpdxLicenseIds,
|
||||
sortIconsCompare,
|
||||
} from '../utils.js';
|
||||
|
||||
const iconsDirectory = path.resolve(import.meta.dirname, '..', '..', 'icons');
|
||||
|
||||
/**
|
||||
* Contains our tests so they can be isolated from each other.
|
||||
* @type {{[k: string]: (data: IconData[], dataString: string) => Promise<string | undefined> | string | undefined}}
|
||||
*/
|
||||
const TESTS = {
|
||||
/**
|
||||
* Tests whether our icons are in alphabetical order.
|
||||
* @param {IconData[]} icons Icons data.
|
||||
* @returns {string|undefined} Error message or undefined.
|
||||
*/
|
||||
alphabetical(icons) {
|
||||
/**
|
||||
* Collects invalid alphabet ordered icons.
|
||||
* @param {IconData[]} invalidEntries Invalid icons reference.
|
||||
* @param {IconData} icon Icon to check.
|
||||
* @param {number} index Index of the icon.
|
||||
* @param {IconData[]} array Array of icons.
|
||||
* @returns {IconData[]} Invalid icons.
|
||||
*/
|
||||
const collector = (invalidEntries, icon, index, array) => {
|
||||
if (index > 0) {
|
||||
const previous = array[index - 1];
|
||||
const comparison = collator.compare(icon.title, previous.title);
|
||||
if (comparison < 0) {
|
||||
invalidEntries.push(icon);
|
||||
} else if (
|
||||
comparison === 0 &&
|
||||
previous.slug &&
|
||||
(!icon.slug || collator.compare(icon.slug, previous.slug) < 0)
|
||||
) {
|
||||
invalidEntries.push(icon);
|
||||
}
|
||||
}
|
||||
|
||||
return invalidEntries;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format an icon for display in the error message.
|
||||
* @param {IconData} icon Icon to format.
|
||||
* @returns {string} Formatted icon.
|
||||
*/
|
||||
const format = (icon) => {
|
||||
if (icon.slug) {
|
||||
return `${icon.title} (${icon.slug})`;
|
||||
}
|
||||
|
||||
return icon.title;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the expected position of an icon.
|
||||
* @param {IconData[]} expectedOrder Expected order of the icons.
|
||||
* @param {IconData} targetIcon Icon to find.
|
||||
* @returns {string} Expected position of the icon.
|
||||
*/
|
||||
const findPositon = (expectedOrder, targetIcon) => {
|
||||
const foundIndex = expectedOrder.findIndex(
|
||||
(icon) =>
|
||||
targetIcon.title === icon.title && targetIcon.slug === icon.slug,
|
||||
);
|
||||
const before = expectedOrder[foundIndex - 1];
|
||||
const after = expectedOrder[foundIndex + 1];
|
||||
if (before) return `should be after ${format(before)}`;
|
||||
if (after) return `should be before ${format(after)}`;
|
||||
return 'not found';
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/no-array-reduce, unicorn/no-array-callback-reference
|
||||
const invalids = icons.reduce(collector, []);
|
||||
if (invalids.length > 0) {
|
||||
const expectedOrder = [...icons].sort(sortIconsCompare);
|
||||
|
||||
return `Some icons aren't in alphabetical order:
|
||||
${invalids.map((icon) => `${format(icon)} ${findPositon(expectedOrder, icon)}`).join('\n')}`;
|
||||
}
|
||||
},
|
||||
|
||||
/* Check the formatting of the data file */
|
||||
prettified(data, dataString) {
|
||||
const normalizedDataString = normalizeNewlines(dataString);
|
||||
const dataPretty = `${JSON.stringify(data, null, '\t')}\n`;
|
||||
|
||||
if (normalizedDataString !== dataPretty) {
|
||||
const dataDiff = fakeDiff(normalizedDataString, dataPretty);
|
||||
return `Data file is formatted incorrectly:\n\n${dataDiff}`;
|
||||
}
|
||||
},
|
||||
|
||||
/* Check redundant trailing slash in URL */
|
||||
checkUrls(icons) {
|
||||
/**
|
||||
* Check if an URL has a redundant trailing slash.
|
||||
* @param {URL} $url URL instance.
|
||||
* @param {string} url Original URL string.
|
||||
* @returns {boolean} Whether the URL has a redundant trailing slash.
|
||||
*/
|
||||
const hasRedundantTrailingSlash = ($url, url) => url === $url.origin + '/';
|
||||
|
||||
/**
|
||||
* Check if an URL is static wikimedia asset URL.
|
||||
* @param {URL} $url URL instance.
|
||||
* @returns {boolean} Whether the URL is static wikimedia asset URL.
|
||||
*/
|
||||
const isStaticWikimediaAssetUrl = ($url) =>
|
||||
$url.hostname === 'upload.wikimedia.org';
|
||||
|
||||
/**
|
||||
* Check if an URL is raw GitHub asset URL.
|
||||
* @param {URL} $url URL instance.
|
||||
* @returns {boolean} Whether the URL is raw GitHub asset URL.
|
||||
*/
|
||||
const isRawGithubAssetUrl = ($url) =>
|
||||
$url.hostname === 'raw.githubusercontent.com';
|
||||
|
||||
/**
|
||||
* Check if URl is user attachment URL.
|
||||
* @param {URL} $url URL instance.
|
||||
* @returns {boolean} Whether the URL is user attachment URL.
|
||||
*/
|
||||
const isGitHubUserAttachmentUrl = ($url) =>
|
||||
$url.hostname === 'github.com' &&
|
||||
$url.pathname.startsWith('/user-attachments/assets');
|
||||
|
||||
/**
|
||||
* Check if an URL is a GitHub URL.
|
||||
* @param {URL} $url URL instance.
|
||||
* @returns {boolean} Whether the URL is a GitHub URL.
|
||||
*/
|
||||
const isGitHubUrl = ($url) => $url.hostname === 'github.com';
|
||||
|
||||
/**
|
||||
* Regex to match a permalink GitHub URL for a file.
|
||||
*/
|
||||
const permalinkGitHubRegex =
|
||||
/^https:\/\/github\.com\/[^/]+\/[^/]+\/(blob\/[a-f\d]{40}\/\S+)|(tree\/[a-f\d]{40}(\/\S+)?)|(((issues)|(pull)|(discussions))\/\d+#((issue)|(issuecomment)|(discussioncomment))-\d+)|(wiki\/\S+\/[a-f\d]{40})$/;
|
||||
|
||||
/**
|
||||
* URLs excluded from the GitHub URL check as are used by GitHub brands.
|
||||
*/
|
||||
const gitHubExcludedUrls = new Set([
|
||||
'https://github.com/logos',
|
||||
'https://github.com/features/actions',
|
||||
'https://github.com/sponsors',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check if an URL is a permanent GitHub URL for a file.
|
||||
* @param {string} url URL string.
|
||||
* @returns {boolean} Whether the URL is a GitHub URL for a file.
|
||||
*/
|
||||
const isPermalinkGitHubFileUrl = (url) => permalinkGitHubRegex.test(url);
|
||||
|
||||
/**
|
||||
* Url fields with a boolean indicating if is an icon source URL.
|
||||
* @type {[boolean, string][]}
|
||||
*/
|
||||
const allUrlFields = [];
|
||||
for (const icon of icons) {
|
||||
allUrlFields.push([true, icon.source]);
|
||||
if (icon.guidelines) {
|
||||
allUrlFields.push([false, icon.guidelines]);
|
||||
}
|
||||
|
||||
if (icon.license !== undefined && Object.hasOwn(icon.license, 'url')) {
|
||||
allUrlFields.push([
|
||||
false,
|
||||
// TODO: `hasOwn` is not currently supported by TS.
|
||||
// See https://github.com/microsoft/TypeScript/issues/44253
|
||||
/** @type {string} */
|
||||
// @ts-expect-error
|
||||
icon.license.url,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const invalidUrls = [];
|
||||
for (const [isSourceUrl, url] of allUrlFields) {
|
||||
const $url = new globalThis.URL(url);
|
||||
|
||||
if (hasRedundantTrailingSlash($url, url)) {
|
||||
invalidUrls.push(fakeDiff(url, $url.origin));
|
||||
}
|
||||
|
||||
if (isGitHubUserAttachmentUrl($url)) continue;
|
||||
|
||||
if (isStaticWikimediaAssetUrl($url)) {
|
||||
const expectedUrl = `https://commons.wikimedia.org/wiki/File:${path.basename($url.pathname)}`;
|
||||
invalidUrls.push(fakeDiff(url, expectedUrl));
|
||||
}
|
||||
|
||||
if (isRawGithubAssetUrl($url)) {
|
||||
const [, owner, repo, hash, ...directory] = $url.pathname.split('/');
|
||||
const expectedUrl = `https://github.com/${owner}/${repo}/blob/${hash}/${directory.join('/')}`;
|
||||
invalidUrls.push(fakeDiff(url, expectedUrl));
|
||||
}
|
||||
|
||||
if (
|
||||
isSourceUrl &&
|
||||
isGitHubUrl($url) &&
|
||||
!isPermalinkGitHubFileUrl(url) &&
|
||||
!gitHubExcludedUrls.has(url)
|
||||
) {
|
||||
invalidUrls.push(
|
||||
`'${url}' must be a permalink GitHub URL. Expecting something like` +
|
||||
" 'https://github.com/<owner>/<repo>/blob/<hash>/<file/path.ext>'.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidUrls.length > 0) {
|
||||
return `Invalid URLs:\n\n${invalidUrls.join('\n\n')}`;
|
||||
}
|
||||
},
|
||||
|
||||
/* Check if all licenses are valid SPDX identifiers */
|
||||
async checkLicense(icons) {
|
||||
const spdxLicenseIds = new Set(await getSpdxLicenseIds());
|
||||
const badLicenses = [];
|
||||
for (const icon of icons) {
|
||||
if (
|
||||
icon.license &&
|
||||
icon.license.type !== 'custom' &&
|
||||
!spdxLicenseIds.has(icon.license.type)
|
||||
) {
|
||||
badLicenses.push(
|
||||
`${icon.title} (${getIconSlug(icon)}) has not a valid SPDX license.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (badLicenses.length > 0) {
|
||||
return `Bad licenses:\n\n${badLicenses.join('\n')}\n\nSee the valid license indentifiers at https://spdx.org/licenses`;
|
||||
}
|
||||
},
|
||||
|
||||
/* Ensure that fields are sorted in the same way for all icons */
|
||||
fieldsSorted(icons) {
|
||||
const formatted = formatIconData(icons);
|
||||
const previous = JSON.stringify(icons, null, '\t');
|
||||
const sorted = JSON.stringify(formatted, null, '\t');
|
||||
if (previous !== sorted) {
|
||||
const diff = fakeDiff(previous, sorted);
|
||||
return `Fields are not sorted in the same way for all icons:\n\n${diff}`;
|
||||
}
|
||||
},
|
||||
|
||||
/* Ensure that aliases constraints are enforced. */
|
||||
checkAliases(icons) {
|
||||
const errors = [];
|
||||
|
||||
for (const icon of icons) {
|
||||
// Old aliases must be different from the title
|
||||
const oldAliases = icon.aliases?.old || [];
|
||||
for (const oldAlias of oldAliases) {
|
||||
if (oldAlias === icon.title) {
|
||||
errors.push(
|
||||
`Icon "${icon.title}" has an alias "old" that is the same as its title.` +
|
||||
' Please remove the alias or change the title.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// AKA aliases must be different from the title
|
||||
const akaAliases = icon.aliases?.aka || [];
|
||||
for (const akaAlias of akaAliases) {
|
||||
if (akaAlias === icon.title) {
|
||||
errors.push(
|
||||
`Icon "${icon.title}" has an alias "aka" that is the same as its title.` +
|
||||
' Please remove the alias or change the title.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Duplicate aliases titles must be different from the title
|
||||
const duplicateAliases = icon.aliases?.dup || [];
|
||||
for (const {title: duplicateAliasTitle} of duplicateAliases) {
|
||||
if (duplicateAliasTitle === icon.title) {
|
||||
errors.push(
|
||||
`Icon "${icon.title}" has a duplicate alias "${duplicateAliasTitle}" that is the same as its title.` +
|
||||
' Please remove the alias or change the title.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Duplicate aliases must be different from each other
|
||||
// based on the title of each one.
|
||||
const duplicateAliasesTitles = duplicateAliases.map(
|
||||
(duplicateAlias) => duplicateAlias.title,
|
||||
);
|
||||
const uniqueDuplicateAliasesTitles = new Set(duplicateAliasesTitles);
|
||||
if (uniqueDuplicateAliasesTitles.size !== duplicateAliasesTitles.length) {
|
||||
errors.push(
|
||||
`Icon "${icon.title}" has duplicate aliases with the same title.` +
|
||||
' Please ensure that all duplicate aliases have unique titles.',
|
||||
);
|
||||
}
|
||||
|
||||
// Localized aliases must be different from the title
|
||||
const locAliases = icon.aliases?.loc || {};
|
||||
for (const [lang, locAlias] of Object.entries(locAliases)) {
|
||||
if (locAlias === icon.title) {
|
||||
errors.push(
|
||||
`Icon "${icon.title}" has a localized alias "${lang}" that is the same as its title.` +
|
||||
' Please remove the alias or change the title.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.join('\n') || undefined;
|
||||
},
|
||||
|
||||
/* Ensure that titles constraints are enforced. */
|
||||
checkTitles(icons) {
|
||||
const titles = new Set();
|
||||
const duplicateTitles = [];
|
||||
for (const icon of icons) {
|
||||
const {title, slug} = icon;
|
||||
|
||||
// Titles of icons that do not have slug must be unique.
|
||||
if (slug === undefined) {
|
||||
if (titles.has(title)) {
|
||||
duplicateTitles.push(title);
|
||||
} else {
|
||||
titles.add(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
if (duplicateTitles.length > 0) {
|
||||
const message =
|
||||
`Found duplicate title for icons that do not have slug: ${duplicateTitles.join(', ')}.` +
|
||||
`\nPlease, ensure that all titles are unique or these icons have proper slugs.`;
|
||||
errors.push(message);
|
||||
}
|
||||
|
||||
return errors.join('\n') || undefined;
|
||||
},
|
||||
|
||||
/* Ensure that slugs constraints are enforced. */
|
||||
async checkSlugs(icons) {
|
||||
const errors = [];
|
||||
|
||||
for (const icon of icons) {
|
||||
const {slug, title} = icon;
|
||||
|
||||
if (slug === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Custom slugs must be necessary. If a title is converted to a slug
|
||||
// and it is the same as the slug, then it is not necessary.
|
||||
if (titleToSlug(title) === slug) {
|
||||
errors.push(
|
||||
`Icon "${title}" has a slug "${slug}" that is the same as the slug inferred from its title.` +
|
||||
' Please, remove the slug.',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Custom slugs must be normalized with almost the same rules that
|
||||
// are used for slugs inferred from titles. We allow underscores in
|
||||
// custom slugs to ensure that they are unique.
|
||||
const normalizedSlug = titleToSlug(slug);
|
||||
const slugWithoutUnderscores = slug.replaceAll('_', '');
|
||||
if (normalizedSlug !== slugWithoutUnderscores) {
|
||||
errors.push(
|
||||
`Icon "${title}" has a slug "${slug}" that is not normalized according to the rules used for titles.` +
|
||||
` Please, rewrite as "${normalizedSlug}" or use another slug.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Icons with custom slugs must have the corresponding icon file.
|
||||
const iconFilePath = path.resolve(iconsDirectory, `${slug}.svg`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const iconFilePathExists = await fileExists(iconFilePath);
|
||||
if (!iconFilePathExists) {
|
||||
errors.push(
|
||||
`Icon "${title}" has a slug "${slug}" but the corresponding icon file "icons/${slug}.svg" does not exist.` +
|
||||
' Please, create the icon file or remove the slug.',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const otherIcon of icons) {
|
||||
if (otherIcon.title === title) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Custom slugs must be unique.
|
||||
if (otherIcon.slug === slug) {
|
||||
errors.push(
|
||||
`Icon "${title}" has a slug "${slug}" that is not unique.` +
|
||||
` It is already used by "${otherIcon.title}". Please, ensure that all slugs are unique.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Custom slugs must be different from slugs inferred from titles.
|
||||
if (slug === titleToSlug(otherIcon.title)) {
|
||||
errors.push(
|
||||
`Icon "${title}" has a slug "${slug}" that is the same as the slug inferred from the title of the icon "${otherIcon.title}".` +
|
||||
' Please, ensure that all slugs are unique.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.join('\n') || undefined;
|
||||
},
|
||||
};
|
||||
|
||||
const iconsDataString = await getIconsDataString();
|
||||
const iconsData = JSON.parse(iconsDataString);
|
||||
|
||||
const errors = (
|
||||
await Promise.all(
|
||||
Object.values(TESTS).map((test) => test(iconsData, iconsDataString)),
|
||||
)
|
||||
)
|
||||
// eslint-disable-next-line unicorn/no-await-expression-member
|
||||
.filter(Boolean);
|
||||
|
||||
if (errors.length > 0) {
|
||||
for (const error of errors) console.error(`\u001B[31m${error}\u001B[0m`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Send release message to Discord #releases channel.
|
||||
*/
|
||||
|
||||
import process from 'node:process';
|
||||
|
||||
const releaseVersion = process.argv[2];
|
||||
const discordReleasesRoleId = process.env.DISCORD_RELEASES_ROLE_ID;
|
||||
const discordReleasesWebhookUrl = process.env.DISCORD_RELEASES_WEBHOOK_URL;
|
||||
const githubReleaseUrl = `https://github.com/simple-icons/simple-icons/releases/tag/${releaseVersion}`;
|
||||
|
||||
if (discordReleasesRoleId === undefined) {
|
||||
console.error('DISCORD_RELEASES_ROLE_ID environment variable is not set.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (discordReleasesWebhookUrl === undefined) {
|
||||
console.error(
|
||||
'DISCORD_RELEASES_WEBHOOK_URL environment variable is not set.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await globalThis.fetch(discordReleasesWebhookUrl, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: `<@&${discordReleasesRoleId}> ${githubReleaseUrl}`,
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
console.error('Failed to send release message to Discord.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Minify data/simple-icons.json file.
|
||||
*/
|
||||
import {getIconSlug, getIconsData} from '../../sdk.mjs';
|
||||
import {formatIconData, writeIconsData} from '../utils.js';
|
||||
|
||||
const plainIcons = await getIconsData();
|
||||
const iconsWithSlugs = plainIcons.map((icon) =>
|
||||
icon.slug ? icon : {...icon, slug: getIconSlug(icon)},
|
||||
);
|
||||
await writeIconsData(formatIconData(iconsWithSlugs), true);
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Rewrite some Markdown files.
|
||||
*/
|
||||
|
||||
import {readFile, writeFile} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
const LINKS_BRANCH = process.argv[2] || 'develop';
|
||||
|
||||
const rootDirectory = path.resolve(import.meta.dirname, '..', '..');
|
||||
const readmeFile = path.resolve(rootDirectory, 'README.md');
|
||||
const disclaimerFile = path.resolve(rootDirectory, 'DISCLAIMER.md');
|
||||
|
||||
/**
|
||||
* Reformat a file.
|
||||
* @param {string} filePath Path to the file.
|
||||
*/
|
||||
const reformat = async (filePath) => {
|
||||
const fileContent = await readFile(filePath, 'utf8');
|
||||
await writeFile(
|
||||
filePath,
|
||||
fileContent
|
||||
// Replace all CDN links with raw links
|
||||
.replaceAll(
|
||||
/https:\/\/cdn.simpleicons.org\/(.+)\/000\/fff/g,
|
||||
`https://raw.githubusercontent.com/simple-icons/simple-icons/${LINKS_BRANCH}/icons/$1.svg`,
|
||||
)
|
||||
// Replace all GitHub blockquotes with regular markdown
|
||||
// Reference: https://github.com/orgs/community/discussions/16925
|
||||
.replaceAll(
|
||||
/\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)](?!\()/g,
|
||||
(string_, $0) => {
|
||||
const capital = $0.slice(0, 1);
|
||||
const body = $0.slice(1).toLowerCase();
|
||||
return `**${capital + body}**`;
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
await Promise.all([reformat(readmeFile), reformat(disclaimerFile)]);
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"lib": ["es2022"],
|
||||
"module": "es2022",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"removeComments": true,
|
||||
"esModuleInterop": true,
|
||||
"checkJs": false,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["../../sdk.mjs"]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Updates the CDN URLs in the README.md to match the major version in the
|
||||
* NPM package manifest. Does nothing if the README.md is already up-to-date.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
const rootDirectory = path.resolve(import.meta.dirname, '..', '..');
|
||||
const packageJsonFile = path.resolve(rootDirectory, 'package.json');
|
||||
const readmeFile = path.resolve(rootDirectory, 'README.md');
|
||||
|
||||
/**
|
||||
* Get the major version number from a semantic version string.
|
||||
* @param {string} semVersion A semantic version string.
|
||||
* @returns {number} The major version number.
|
||||
*/
|
||||
const getMajorVersion = (semVersion) => {
|
||||
const majorVersionAsString = semVersion.split('.')[0];
|
||||
return Number.parseInt(majorVersionAsString, 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the package.json manifest.
|
||||
* @returns {Promise<{version: string}>} The package.json manifest.
|
||||
*/
|
||||
const getManifest = async () => {
|
||||
const manifestRaw = await fs.readFile(packageJsonFile, 'utf8');
|
||||
return JSON.parse(manifestRaw);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the version number in the README.md.
|
||||
* @param {number} majorVersion The major version number.
|
||||
*/
|
||||
const updateVersionInReadmeIfNecessary = async (majorVersion) => {
|
||||
let content = await fs.readFile(readmeFile, 'utf8');
|
||||
|
||||
content = content.replaceAll(
|
||||
/simple-icons@v\d+/g,
|
||||
`simple-icons@v${majorVersion}`,
|
||||
);
|
||||
|
||||
await fs.writeFile(readmeFile, content);
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
try {
|
||||
const manifest = await getManifest();
|
||||
const majorVersion = getMajorVersion(manifest.version);
|
||||
await updateVersionInReadmeIfNecessary(majorVersion);
|
||||
} catch (error) {
|
||||
console.error('Failed to update CDN version number:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Updates the SDK Typescript definitions located in the file sdk.d.ts
|
||||
* to match the current definitions of functions of sdk.mjs.
|
||||
*/
|
||||
|
||||
import {spawnSync} from 'node:child_process';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import {fileExists} from '../utils.js';
|
||||
|
||||
const rootDirectory = path.resolve(import.meta.dirname, '..', '..');
|
||||
|
||||
const sdkTs = path.resolve(rootDirectory, 'sdk.d.ts');
|
||||
const sdkMts = path.resolve(rootDirectory, 'sdk.d.mts');
|
||||
const sdkMjs = path.resolve(rootDirectory, 'sdk.mjs');
|
||||
|
||||
const generateSdkMts = async () => {
|
||||
// Remove temporally type definitions imported with comments
|
||||
// in sdk.mjs to avoid circular imports
|
||||
const originalSdkMjsContent = await fs.readFile(sdkMjs, 'utf8');
|
||||
const temporarySdkMjsContent = originalSdkMjsContent
|
||||
.split('\n')
|
||||
.filter((line) => !line.startsWith(' * @typedef {import("./sdk")'))
|
||||
.join('\n');
|
||||
await fs.writeFile(sdkMjs, temporarySdkMjsContent);
|
||||
|
||||
let cmd;
|
||||
let error = false;
|
||||
try {
|
||||
cmd = spawnSync(
|
||||
'npx',
|
||||
[
|
||||
'tsc',
|
||||
'-p',
|
||||
path.join('scripts', 'release', 'sdk-ts-defs-jsconfig.json'),
|
||||
'--declaration',
|
||||
'--emitDeclarationOnly',
|
||||
],
|
||||
{stdio: 'inherit'},
|
||||
);
|
||||
} catch {
|
||||
await fs.writeFile(sdkMjs, originalSdkMjsContent);
|
||||
error = true;
|
||||
}
|
||||
|
||||
let errorMessage = "Command 'npx tsc sdk.mjs' failed for an unknown reason";
|
||||
if (cmd && cmd.stderr) {
|
||||
errorMessage = cmd.stderr.toString();
|
||||
}
|
||||
|
||||
if (error || cmd === undefined) {
|
||||
process.stdout.write(
|
||||
`Error generating Typescript definitions: '${errorMessage}'\n`,
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await fs.writeFile(sdkMjs, originalSdkMjsContent);
|
||||
};
|
||||
|
||||
/**
|
||||
* We must remove the duplicated export types that tsc generates from
|
||||
* JSDoc `typedef` comments.
|
||||
* See {@link https://github.com/microsoft/TypeScript/issues/46011}.
|
||||
* @param {string} content Content of the file.
|
||||
* @returns {string} The content without duplicated export types.
|
||||
*/
|
||||
const removeDuplicatedExportTypes = (content) => {
|
||||
const newContent = [];
|
||||
const lines = content.split('\n');
|
||||
/** @type {string[]} */
|
||||
const exportTypesFound = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('export type ')) {
|
||||
const type = line.split(' ')[2];
|
||||
if (!exportTypesFound.includes(type)) {
|
||||
newContent.push(line);
|
||||
exportTypesFound.push(type);
|
||||
}
|
||||
} else {
|
||||
newContent.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return newContent.join('\n');
|
||||
};
|
||||
|
||||
const generateSdkTs = async () => {
|
||||
const sdkMtsExists = await fileExists(sdkMts);
|
||||
if (sdkMtsExists) await fs.unlink(sdkMts);
|
||||
await generateSdkMts();
|
||||
|
||||
const autogeneratedMessage =
|
||||
'/* The next code is autogenerated from sdk.mjs */\n/* eslint-disable */';
|
||||
const newSdkTsContent =
|
||||
// eslint-disable-next-line unicorn/no-await-expression-member
|
||||
(await fs.readFile(sdkTs, 'utf8')).split(autogeneratedMessage)[0] +
|
||||
`${autogeneratedMessage}\n\n${await fs.readFile(sdkMts, 'utf8')}`;
|
||||
|
||||
await fs.writeFile(sdkTs, removeDuplicatedExportTypes(newSdkTsContent));
|
||||
await fs.unlink(sdkMts);
|
||||
|
||||
let cmd;
|
||||
let error = false;
|
||||
try {
|
||||
cmd = spawnSync('npx', ['prettier', '-w', 'sdk.d.ts'], {stdio: 'inherit'});
|
||||
} catch {
|
||||
error = true;
|
||||
}
|
||||
|
||||
let errorMessage =
|
||||
"Command 'npx prettier -w sdk.d.ts' failed for an unknown reason";
|
||||
if (cmd && cmd.stderr) {
|
||||
errorMessage = cmd.stderr.toString();
|
||||
}
|
||||
|
||||
if (error || cmd === undefined) {
|
||||
process.stdout.write(
|
||||
'Error executing Prettier to prettify' +
|
||||
` SDK TS definitions: '${errorMessage}'\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
await generateSdkTs();
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Generates a MarkDown file that lists every brand name and their slug.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {getIconSlug, getIconsData} from '../../sdk.mjs';
|
||||
|
||||
const rootDirectory = path.resolve(import.meta.dirname, '..', '..');
|
||||
const slugsFile = path.resolve(rootDirectory, 'slugs.md');
|
||||
|
||||
let content = `<!--
|
||||
|
||||
update the script at '${path.relative(rootDirectory, import.meta.filename)}'.
|
||||
-->
|
||||
|
||||
# Simple Icons slugs
|
||||
|
||||
| Brand name | Brand slug |
|
||||
| :--- | :--- |
|
||||
`;
|
||||
|
||||
const icons = await getIconsData();
|
||||
for (const icon of icons) {
|
||||
const brandName = icon.title;
|
||||
const brandSlug = getIconSlug(icon);
|
||||
content += `| \`${brandName}\` | \`${brandSlug}\` |\n`;
|
||||
}
|
||||
|
||||
await fs.writeFile(slugsFile, content);
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Replaces the SVG count milestone "Over <NUMBER> SVG icons..." located
|
||||
* at README every time the number of current icons is more than `updateRange`
|
||||
* more than the previous milestone.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import {getIconsData} from '../../sdk.mjs';
|
||||
|
||||
const regexMatcher = /Over\s(\d+)\s/;
|
||||
const updateRange = 100;
|
||||
|
||||
const rootDirectory = path.resolve(import.meta.dirname, '..', '..');
|
||||
const readmeFile = path.resolve(rootDirectory, 'README.md');
|
||||
|
||||
const readmeContent = await fs.readFile(readmeFile, 'utf8');
|
||||
|
||||
try {
|
||||
const match = regexMatcher.exec(readmeContent);
|
||||
if (match === null) {
|
||||
console.error(
|
||||
'Failed to obtain number of SVG icons of current milestone in README:',
|
||||
'No match found',
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
const overNIconsInReadme = Number.parseInt(match[1], 10);
|
||||
const iconsData = await getIconsData();
|
||||
const nIcons = iconsData.length;
|
||||
const nIconsRounded = Math.floor(nIcons / updateRange) * updateRange;
|
||||
|
||||
if (overNIconsInReadme !== nIconsRounded) {
|
||||
const newContent = readmeContent.replace(
|
||||
regexMatcher,
|
||||
`Over ${nIconsRounded} `,
|
||||
);
|
||||
await fs.writeFile(readmeFile, newContent);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to update number of SVG icons of current milestone in README:',
|
||||
error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
// @ts-check
|
||||
/**
|
||||
* @file
|
||||
* Script to remove an icon and its data.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import {search} from '@inquirer/prompts';
|
||||
import {search as fuzzySearch} from 'fast-fuzzy';
|
||||
import {getIconSlug, getIconsData} from '../sdk.mjs';
|
||||
import {writeIconsData} from './utils.js';
|
||||
|
||||
process.exitCode = 1;
|
||||
process.on('uncaughtException', (error) => {
|
||||
if (error instanceof Error && error.name === 'ExitPromptError') {
|
||||
process.stdout.write('\nAborted\n');
|
||||
process.exit(1);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
const rootDirectory = path.resolve(import.meta.dirname, '..');
|
||||
const svgFilesDirectory = path.resolve(rootDirectory, 'icons');
|
||||
|
||||
const iconsData = await getIconsData();
|
||||
const icons = iconsData.map((icon, index) => {
|
||||
const slug = getIconSlug(icon);
|
||||
return {
|
||||
name: `${icon.title} (${slug})`,
|
||||
value: {title: icon.title, slug, index},
|
||||
};
|
||||
});
|
||||
|
||||
const found = await search({
|
||||
message: 'Search for an icon to remove:',
|
||||
async source(input) {
|
||||
if (!input) return [];
|
||||
return fuzzySearch(input, icons, {
|
||||
keySelector: (icon) => [icon.value.title, icon.value.slug],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
console.error('No icon selected.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
iconsData.splice(found.index, 1);
|
||||
await writeIconsData(iconsData);
|
||||
await fs.unlink(path.resolve(svgFilesDirectory, `${found.slug}.svg`));
|
||||
process.stdout.write(`Icon "${found.title} (${found.slug}.svg)" removed.\n`);
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,195 @@
|
||||
// @ts-check
|
||||
/* eslint jsdoc/reject-any-type: off */
|
||||
/**
|
||||
* @file Internal utilities.
|
||||
*
|
||||
* Here resides all the functionality that does not qualifies to reside
|
||||
* in the SDK because is not publicly exposed.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {collator, getIconSlug, getIconsDataPath, titleToSlug} from '../sdk.mjs';
|
||||
|
||||
/**
|
||||
* @typedef {import("../types.js").IconData} IconData
|
||||
* @typedef {import("../types.js").DuplicateAlias} DuplicateAlias
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get JSON schema data.
|
||||
* @returns {Promise<any>} JSON schema data.
|
||||
*/
|
||||
export const getJsonSchemaData = async () =>
|
||||
JSON.parse(
|
||||
await fs.readFile(
|
||||
path.resolve(import.meta.dirname, '..', '.jsonschema.json'),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Write icons data to data/simple-icons.json.
|
||||
* @param {IconData[]} iconsData Icons data array.
|
||||
* @param {boolean} [minify] Whether to minify the JSON output.
|
||||
*/
|
||||
export const writeIconsData = async (iconsData, minify = false) => {
|
||||
await fs.writeFile(
|
||||
getIconsDataPath(),
|
||||
`${JSON.stringify(iconsData, null, minify ? 0 : '\t')}\n`,
|
||||
'utf8',
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get SPDX license IDs from `spdx-license-ids` package.
|
||||
* @returns {Promise<string[]>} Set of SPDX license IDs.
|
||||
*/
|
||||
export const getSpdxLicenseIds = async () =>
|
||||
JSON.parse(
|
||||
await fs.readFile(
|
||||
path.resolve(
|
||||
import.meta.dirname,
|
||||
'..',
|
||||
'node_modules',
|
||||
'spdx-license-ids',
|
||||
'index.json',
|
||||
),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* The compare function for sorting icons in *data/simple-icons.json*.
|
||||
* @param {IconData} a Icon A.
|
||||
* @param {IconData} b Icon B.
|
||||
* @returns {number} Comparison result.
|
||||
*/
|
||||
export const sortIconsCompare = (a, b) =>
|
||||
a.title === b.title
|
||||
? collator.compare(getIconSlug(a), getIconSlug(b))
|
||||
: collator.compare(a.title, b.title);
|
||||
|
||||
/**
|
||||
* The compare function for sorting icon duplicate aliases in *data/simple-icons.json*.
|
||||
* @param {DuplicateAlias} a Duplicate alias A.
|
||||
* @param {DuplicateAlias} b Duplicate alias B.
|
||||
* @returns {number} Comparison result.
|
||||
*/
|
||||
const sortDuplicatesCompare = (a, b) =>
|
||||
a.title === b.title
|
||||
? collator.compare(titleToSlug(a.title), titleToSlug(b.title))
|
||||
: collator.compare(a.title, b.title);
|
||||
|
||||
/**
|
||||
* Sort icon data or duplicate alias object.
|
||||
* @template {IconData | DuplicateAlias} T Either icon data or duplicate alias.
|
||||
* @param {T} icon The icon data or duplicate alias as it appears in *data/simple-icons.json*.
|
||||
* @returns {T} The sorted icon data or duplicate alias.
|
||||
*/
|
||||
const sortIconOrDuplicate = (icon) => {
|
||||
const keyOrder = [
|
||||
'title',
|
||||
'slug',
|
||||
'hex',
|
||||
'source',
|
||||
'guidelines',
|
||||
'license',
|
||||
'aliases',
|
||||
// This is not appears in icon data but it's in the alias object.
|
||||
'loc',
|
||||
];
|
||||
|
||||
/** @type {T} */
|
||||
const sortedIcon = Object.assign(
|
||||
Object.fromEntries(
|
||||
Object.entries(icon).sort(
|
||||
([key1], [key2]) => keyOrder.indexOf(key1) - keyOrder.indexOf(key2),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return sortedIcon;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort license object.
|
||||
* @param {IconData['license']} license The license object as it appears in *data/simple-icons.json*.
|
||||
* @returns {IconData['license']} The sorted license object.
|
||||
*/
|
||||
const sortLicense = (license) => {
|
||||
if (!license) return undefined;
|
||||
const keyOrder = ['type', 'url'];
|
||||
|
||||
/** @type {IconData['license']} */
|
||||
const sortedLicense = Object.assign(
|
||||
Object.fromEntries(
|
||||
Object.entries(license).sort(
|
||||
([key1], [key2]) => keyOrder.indexOf(key1) - keyOrder.indexOf(key2),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return sortedLicense;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort object key alphabetically.
|
||||
* @param {IconData['aliases']} object The aliases object as it appears in *data/simple-icons.json*.
|
||||
* @returns {{[_: string]: string} | undefined} The sorted aliases object.
|
||||
*/
|
||||
const sortAlphabetically = (object) => {
|
||||
if (!object) return undefined;
|
||||
const sorted = Object.assign(
|
||||
Object.fromEntries(
|
||||
Object.entries(object).sort(([key1], [key2]) => (key1 > key2 ? 1 : -1)),
|
||||
),
|
||||
);
|
||||
return sorted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort icons data.
|
||||
* @param {IconData[]} iconsData The icons data as it appears in *data/simple-icons.json*.
|
||||
* @returns {IconData[]} The sorted icons data.
|
||||
*/
|
||||
export const formatIconData = (iconsData) => {
|
||||
const iconsDataCopy = structuredClone(iconsData);
|
||||
const icons = iconsDataCopy.map((icon) =>
|
||||
sortIconOrDuplicate({
|
||||
...icon,
|
||||
license: sortLicense(icon.license),
|
||||
aliases: icon.aliases
|
||||
? sortAlphabetically({
|
||||
aka: icon.aliases.aka?.sort(collator.compare),
|
||||
dup: icon.aliases.dup
|
||||
? icon.aliases.dup.sort(sortDuplicatesCompare).map((d) =>
|
||||
sortIconOrDuplicate({
|
||||
...d,
|
||||
loc: sortAlphabetically(d.loc),
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
loc: sortAlphabetically(icon.aliases.loc),
|
||||
old: icon.aliases.old?.sort(collator.compare),
|
||||
})
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
icons.sort(sortIconsCompare);
|
||||
return icons;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a file exists.
|
||||
* @param {string} fpath File path to check.
|
||||
* @returns {Promise<boolean>} True if the file exists, false otherwise.
|
||||
*/
|
||||
export const fileExists = async (fpath) => {
|
||||
try {
|
||||
await fs.access(fpath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user