diff --git a/.svglintrc.mjs b/.svglintrc.mjs index e20d019bf..861f200f2 100644 --- a/.svglintrc.mjs +++ b/.svglintrc.mjs @@ -1,6 +1,8 @@ -import fs from 'node:fs'; +import fs from 'node:fs/promises'; import path from 'node:path'; +import process from 'node:process'; import { + SVG_PATH_REGEX, getDirnameFromImportMeta, htmlFriendlyToTitle, collator, @@ -19,16 +21,17 @@ const htmlNamedEntitiesFile = path.join( ); const svglintIgnoredFile = path.join(__dirname, '.svglint-ignored.json'); -const data = JSON.parse(fs.readFileSync(dataFile, 'utf8')); +const data = JSON.parse(await fs.readFile(dataFile, 'utf8')); const htmlNamedEntities = JSON.parse( - fs.readFileSync(htmlNamedEntitiesFile, 'utf8'), + await fs.readFile(htmlNamedEntitiesFile, 'utf8'), +); +const svglintIgnores = JSON.parse( + await fs.readFile(svglintIgnoredFile, 'utf8'), ); -const svglintIgnores = JSON.parse(fs.readFileSync(svglintIgnoredFile, 'utf8')); const svgRegexp = /^.*<\/title><path d=".*"\/><\/svg>$/; const negativeZerosRegexp = /-0(?=[^\.]|[\s\d\w]|$)/g; -const svgPathRegexp = /^[Mm][MmZzLlHhVvCcSsQqTtAaEe0-9\-,. ]+$/; const iconSize = 24; const iconTargetCenter = iconSize / 2; @@ -140,14 +143,14 @@ const getIconPathSegments = memoize((iconPath) => parsePath(iconPath)); const getIconPathBbox = memoize((iconPath) => svgPathBbox(iconPath)); if (updateIgnoreFile) { - process.on('exit', () => { + process.on('exit', async () => { // ensure object output order is consistent due to async svglint processing const sorted = sortObjectByKey(iconIgnored); for (const linterName in sorted) { sorted[linterName] = sortObjectByValue(sorted[linterName]); } - fs.writeFileSync(ignoreFile, JSON.stringify(sorted, null, 2) + '\n', { + await fs.writeFile(ignoreFile, JSON.stringify(sorted, null, 2) + '\n', { flag: 'w', }); }); @@ -197,7 +200,7 @@ export default { { // ensure that the path element only has the 'd' attribute // (no style, opacity, etc.) - d: svgPathRegexp, + d: SVG_PATH_REGEX, 'rule::selector': 'svg > path', 'rule::whitelist': true, }, @@ -908,7 +911,7 @@ export default { const iconPath = getIconPath($, filepath); - if (!svgPathRegexp.test(iconPath)) { + if (!SVG_PATH_REGEX.test(iconPath)) { let errorMsg = 'Invalid path format', reason; @@ -920,7 +923,7 @@ export default { reporter.error(`${errorMsg}: ${reason}`); } - const validPathCharacters = svgPathRegexp.source.replace( + const validPathCharacters = SVG_PATH_REGEX.source.replace( /[\[\]+^$]/g, '', ), diff --git a/_data/simple-icons.json b/_data/simple-icons.json index b39a7fe86..bfb76a238 100644 --- a/_data/simple-icons.json +++ b/_data/simple-icons.json @@ -11461,7 +11461,7 @@ { "title": "SmugMug", "hex": "6DB944", - "source": "https://help.smugmug.com/using-smugmug's-logo-HJulJePkEBf" + "source": "https://www.smugmughelp.com/articles/409-smugmug-s-logo-and-usage" }, { "title": "Snapchat", diff --git a/scripts/add-icon-data.js b/scripts/add-icon-data.js index c816a26e6..cc9b903a1 100644 --- a/scripts/add-icon-data.js +++ b/scripts/add-icon-data.js @@ -1,3 +1,4 @@ +import process from 'node:process'; import chalk from 'chalk'; import { input, confirm, checkbox } from '@inquirer/prompts'; import getRelativeLuminance from 'get-relative-luminance'; @@ -27,10 +28,10 @@ const titleValidator = (text) => { }; const hexValidator = (text) => - hexPattern.test(text) ? true : 'This should be a valid hex code'; + hexPattern.test(text) || 'This should be a valid hex code'; const sourceValidator = (text) => - URL_REGEX.test(text) ? true : 'This should be a secure URL'; + URL_REGEX.test(text) || 'This should be a secure URL'; const hexTransformer = (text) => { const color = normalizeColor(text); diff --git a/scripts/build/package.js b/scripts/build/package.js index 4a88e8a90..05ca17bba 100644 --- a/scripts/build/package.js +++ b/scripts/build/package.js @@ -40,9 +40,6 @@ const build = async () => { const escape = (value) => { return value.replace(/(?<!\\)'/g, "\\'"); }; - const iconToKeyValue = (icon) => { - return `'${icon.slug}':${iconToObject(icon)}`; - }; const licenseToObject = (license) => { if (license === undefined) { return; @@ -82,7 +79,7 @@ const build = async () => { icons.map(async (icon) => { const filename = getIconSlug(icon); const svgFilepath = path.resolve(iconsDir, `${filename}.svg`); - icon.svg = (await fs.readFile(svgFilepath, UTF8)).replace(/\r?\n/, ''); + icon.svg = await fs.readFile(svgFilepath, UTF8); icon.path = svgToPath(icon.svg); icon.slug = filename; const iconObject = iconToObject(icon); @@ -96,11 +93,11 @@ const build = async () => { const iconsBarrelMjs = []; buildIcons.sort((a, b) => collator.compare(a.icon.title, b.icon.title)); - buildIcons.forEach(({ iconObject, iconExportName }) => { + for (const { iconObject, iconExportName } of buildIcons) { iconsBarrelDts.push(`export const ${iconExportName}:I;`); iconsBarrelJs.push(`${iconExportName}:${iconObject},`); iconsBarrelMjs.push(`export const ${iconExportName}=${iconObject}`); - }); + } // 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='';`; diff --git a/scripts/get-filename.js b/scripts/get-filename.js index 0cf5369d9..826b43c3e 100644 --- a/scripts/get-filename.js +++ b/scripts/get-filename.js @@ -4,6 +4,7 @@ * icon SVG filename to standard output. */ +import process from 'node:process'; import { titleToSlug } from '../sdk.mjs'; if (process.argv.length < 3) { diff --git a/scripts/lint/jsonlint.js b/scripts/lint/jsonlint.js index b7f93fb28..0479dc988 100644 --- a/scripts/lint/jsonlint.js +++ b/scripts/lint/jsonlint.js @@ -3,22 +3,18 @@ * CLI tool to run jsonschema on the simple-icons.json data file. */ -import path from 'node:path'; +import process from 'node:process'; import { Validator } from 'jsonschema'; -import { getDirnameFromImportMeta, getIconsData } from '../../sdk.mjs'; +import { getIconsData } from '../../sdk.mjs'; import { getJsonSchemaData } from '../utils.js'; const icons = await getIconsData(); -const __dirname = getDirnameFromImportMeta(import.meta.url); -const schema = await getJsonSchemaData(path.resolve(__dirname, '..', '..')); +const schema = await getJsonSchemaData(); const validator = new Validator(); const result = validator.validate({ icons }, schema); if (result.errors.length > 0) { - result.errors.forEach((error) => { - console.error(error); - }); - + result.errors.forEach((error) => console.error(error)); console.error(`Found ${result.errors.length} error(s) in simple-icons.json`); process.exit(1); } diff --git a/scripts/lint/ourlint.js b/scripts/lint/ourlint.js index 85dabd6c1..49abcb206 100644 --- a/scripts/lint/ourlint.js +++ b/scripts/lint/ourlint.js @@ -4,6 +4,7 @@ * linters (e.g. jsonlint/svglint). */ +import process from 'node:process'; import { URL } from 'node:url'; import fakeDiff from 'fake-diff'; import { getIconsDataString, normalizeNewlines, collator } from '../../sdk.mjs'; @@ -46,7 +47,7 @@ const TESTS = { }, /* Check the formatting of the data file */ - prettified: async (data, dataString) => { + prettified: (data, dataString) => { const normalizedDataString = normalizeNewlines(dataString); const dataPretty = `${JSON.stringify(data, null, 4)}\n`; @@ -66,8 +67,7 @@ const TESTS = { const allUrlFields = [ ...new Set( data.icons - .map((icon) => [icon.source, icon.guidelines, icon.license?.url]) - .flat() + .flatMap((icon) => [icon.source, icon.guidelines, icon.license?.url]) .filter(Boolean), ), ]; @@ -84,19 +84,14 @@ const TESTS = { }, }; -// execute all tests and log all errors -(async () => { - const dataString = await getIconsDataString(); - const data = JSON.parse(dataString); +const dataString = await getIconsDataString(); +const data = JSON.parse(dataString); - const errors = ( - await Promise.all( - Object.keys(TESTS).map((test) => TESTS[test](data, dataString)), - ) - ).filter(Boolean); +const errors = ( + await Promise.all(Object.values(TESTS).map((test) => test(data, dataString))) +).filter(Boolean); - if (errors.length > 0) { - errors.forEach((error) => console.error(`\u001b[31m${error}\u001b[0m`)); - process.exit(1); - } -})(); +if (errors.length > 0) { + errors.forEach((error) => console.error(`\u001b[31m${error}\u001b[0m`)); + process.exit(1); +} diff --git a/scripts/release/update-cdn-urls.js b/scripts/release/update-cdn-urls.js index 6b0a52c85..f6be9e9ba 100644 --- a/scripts/release/update-cdn-urls.js +++ b/scripts/release/update-cdn-urls.js @@ -4,7 +4,8 @@ * NPM package manifest. Does nothing if the README.md is already up-to-date. */ -import fs from 'node:fs'; +import process from 'node:process'; +import fs from 'node:fs/promises'; import path from 'node:path'; import { getDirnameFromImportMeta } from '../../sdk.mjs'; @@ -19,31 +20,31 @@ const getMajorVersion = (semVerVersion) => { return parseInt(majorVersionAsString); }; -const getManifest = () => { - const manifestRaw = fs.readFileSync(packageJsonFile, 'utf-8'); +const getManifest = async () => { + const manifestRaw = await fs.readFile(packageJsonFile, 'utf-8'); return JSON.parse(manifestRaw); }; -const updateVersionInReadmeIfNecessary = (majorVersion) => { - let content = fs.readFileSync(readmeFile).toString(); +const updateVersionInReadmeIfNecessary = async (majorVersion) => { + let content = await fs.readFile(readmeFile, 'utf8'); content = content.replace( /simple-icons@v[0-9]+/g, `simple-icons@v${majorVersion}`, ); - fs.writeFileSync(readmeFile, content); + await fs.writeFile(readmeFile, content); }; -const main = () => { +const main = async () => { try { - const manifest = getManifest(); + const manifest = await getManifest(); const majorVersion = getMajorVersion(manifest.version); - updateVersionInReadmeIfNecessary(majorVersion); + await updateVersionInReadmeIfNecessary(majorVersion); } catch (error) { console.error('Failed to update CDN version number:', error); process.exit(1); } }; -main(); +await main(); diff --git a/scripts/release/update-sdk-ts-defs.js b/scripts/release/update-sdk-ts-defs.js index 6e6a3eec9..c87e87625 100644 --- a/scripts/release/update-sdk-ts-defs.js +++ b/scripts/release/update-sdk-ts-defs.js @@ -4,7 +4,7 @@ * to match the current definitions of functions of sdk.mjs. */ -import fsSync from 'node:fs'; +import process from 'node:process'; import fs from 'node:fs/promises'; import path from 'node:path'; import { execSync } from 'node:child_process'; @@ -45,7 +45,11 @@ const generateSdkMts = async () => { }; const generateSdkTs = async () => { - fsSync.existsSync(sdkMts) && (await fs.unlink(sdkMts)); + const fileExists = await fs + .access(sdkMts) + .then(() => true) + .catch(() => false); + fileExists && (await fs.unlink(sdkMts)); await generateSdkMts(); const autogeneratedMsg = '/* The next code is autogenerated from sdk.mjs */'; diff --git a/scripts/release/update-slugs-table.js b/scripts/release/update-slugs-table.js index 7f99a9c10..fffe85f7e 100644 --- a/scripts/release/update-slugs-table.js +++ b/scripts/release/update-slugs-table.js @@ -25,14 +25,10 @@ update the script at '${path.relative(rootDir, __filename)}'. | :--- | :--- | `; -(async () => { - const icons = await getIconsData(); - - icons.forEach((icon) => { - const brandName = icon.title; - const brandSlug = getIconSlug(icon); - content += `| \`${brandName}\` | \`${brandSlug}\` |\n`; - }); - - await fs.writeFile(slugsFile, content); -})(); +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); diff --git a/scripts/release/update-svgs-count.js b/scripts/release/update-svgs-count.js index dfb2ea461..05af0fc17 100644 --- a/scripts/release/update-svgs-count.js +++ b/scripts/release/update-svgs-count.js @@ -4,7 +4,9 @@ * at README every time the number of current icons is more than `updateRange` * more than the previous milestone. */ -import { promises as fs } from 'node:fs'; + +import process from 'node:process'; +import fs from 'node:fs/promises'; import path from 'node:path'; import { getDirnameFromImportMeta, getIconsData } from '../../sdk.mjs'; @@ -12,31 +14,26 @@ const regexMatcher = /Over\s(\d+)\s/; const updateRange = 100; const __dirname = getDirnameFromImportMeta(import.meta.url); - const rootDir = path.resolve(__dirname, '..', '..'); const readmeFile = path.resolve(rootDir, 'README.md'); -(async () => { - const readmeContent = await fs.readFile(readmeFile, 'utf-8'); +const readmeContent = await fs.readFile(readmeFile, 'utf-8'); - let overNIconsInReadme; - try { - overNIconsInReadme = parseInt(regexMatcher.exec(readmeContent)[1]); - } catch (err) { - console.error( - 'Failed to obtain number of SVG icons of current milestone in README:', - err, - ); - process.exit(1); - } +let overNIconsInReadme; +try { + overNIconsInReadme = parseInt(regexMatcher.exec(readmeContent)[1]); +} catch (err) { + console.error( + 'Failed to obtain number of SVG icons of current milestone in README:', + err, + ); + process.exit(1); +} - const nIcons = (await getIconsData()).length; - const newNIcons = overNIconsInReadme + updateRange; - - if (nIcons <= newNIcons) { - process.exit(0); - } +const nIcons = (await getIconsData()).length; +const newNIcons = overNIconsInReadme + updateRange; +if (nIcons > newNIcons) { const newContent = readmeContent.replace(regexMatcher, `Over ${newNIcons} `); await fs.writeFile(readmeFile, newContent); -})(); +} diff --git a/scripts/utils.js b/scripts/utils.js index c0b2c913f..dfea7d75a 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -6,7 +6,7 @@ const __dirname = getDirnameFromImportMeta(import.meta.url); /** * Get JSON schema data. - * @param {String|undefined} rootDir Path to the root directory of the project. + * @param {String} rootDir Path to the root directory of the project. */ export const getJsonSchemaData = async ( rootDir = path.resolve(__dirname, '..'), @@ -19,13 +19,13 @@ export const getJsonSchemaData = async ( /** * Write icons data to _data/simple-icons.json. * @param {Object} iconsData Icons data object. - * @param {String|undefined} rootDir Path to the root directory of the project. + * @param {String} rootDir Path to the root directory of the project. */ export const writeIconsData = async ( iconsData, rootDir = path.resolve(__dirname, '..'), ) => { - return fs.writeFile( + await fs.writeFile( getIconDataPath(rootDir), `${JSON.stringify(iconsData, null, 4)}\n`, 'utf8', diff --git a/sdk.d.ts b/sdk.d.ts index e4c085918..9d920813e 100644 --- a/sdk.d.ts +++ b/sdk.d.ts @@ -62,6 +62,7 @@ export type IconData = { /* The next code is autogenerated from sdk.mjs */ export const URL_REGEX: RegExp; +export const SVG_PATH_REGEX: RegExp; export function getDirnameFromImportMeta(importMetaUrl: string): string; export function getIconSlug(icon: IconData): string; export function svgToPath(svg: string): string; diff --git a/sdk.mjs b/sdk.mjs index 8415b9afd..1b07f9f24 100644 --- a/sdk.mjs +++ b/sdk.mjs @@ -36,7 +36,12 @@ const TITLE_TO_SLUG_RANGE_REGEX = /[^a-z0-9]/g; /** * Regex to validate HTTPs URLs. */ -export const URL_REGEX = /^https:\/\/[^\s]+$/; +export const URL_REGEX = /^https:\/\/[^\s"']+$/; + +/** + * Regex to validate SVG paths. + */ +export const SVG_PATH_REGEX = /^m[-mzlhvcsqtae0-9,. ]+$/i; /** * Get the directory name where this file is located from `import.meta.url`, @@ -59,7 +64,7 @@ export const getIconSlug = (icon) => icon.slug || titleToSlug(icon.title); * @param {String} svg The icon SVG content * @returns {String} The path from the icon SVG content **/ -export const svgToPath = (svg) => svg.match(/ svg.split('"', 8)[7]; /** * Converts a brand title into a slug/filename. @@ -83,8 +88,7 @@ export const titleToSlug = (title) => */ export const slugToVariableName = (slug) => { const slugFirstLetter = slug[0].toUpperCase(); - const slugRest = slug.slice(1); - return `si${slugFirstLetter}${slugRest}`; + return `si${slugFirstLetter}${slug.slice(1)}`; }; /** @@ -189,13 +193,11 @@ export const getThirdPartyExtensions = async ( ) => normalizeNewlines(await fs.readFile(readmePath, 'utf8')) .split('## Third-Party Extensions\n\n')[1] - .split('\n\n')[0] + .split('\n\n', 1)[0] .split('\n') .slice(2) .map((line) => { let [module, author] = line.split(' | '); - - // README shipped with package has not Github theme image links module = module.split(' { + for (const match of docsFileContent.matchAll(getLinksRegex)) { const link = match[0]; assert.ok( ignoreHttpLinks.includes(link) || link.startsWith('https://'), `Link '${link}' in '${docsFile}' (at index ${match.index})` + ` must use the HTTPS protocol.`, ); - }); + } } }); diff --git a/tests/index.test.js b/tests/index.test.js index a8f7c413d..19e735ff6 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -2,14 +2,10 @@ import { getIconsData, getIconSlug, slugToVariableName } from '../sdk.mjs'; import * as simpleIcons from '../index.mjs'; import { testIcon } from './test-icon.js'; -(async () => { - const icons = await getIconsData(); +for (const icon of await getIconsData()) { + const slug = getIconSlug(icon); + const variableName = slugToVariableName(slug); + const subject = simpleIcons[variableName]; - icons.map((icon) => { - const slug = getIconSlug(icon); - const variableName = slugToVariableName(slug); - const subject = simpleIcons[variableName]; - - testIcon(icon, subject, slug); - }); -})(); + testIcon(icon, subject, slug); +} diff --git a/tests/test-icon.js b/tests/test-icon.js index 87861984f..c3ade33d4 100644 --- a/tests/test-icon.js +++ b/tests/test-icon.js @@ -1,15 +1,28 @@ -import fs from 'node:fs'; +import fs from 'node:fs/promises'; import path from 'node:path'; import { strict as assert } from 'node:assert'; import { describe, it } from 'mocha'; -import { URL_REGEX, titleToSlug } from '../sdk.mjs'; +import { + SVG_PATH_REGEX, + URL_REGEX, + getDirnameFromImportMeta, + titleToSlug, +} from '../sdk.mjs'; -const iconsDir = path.resolve(process.cwd(), 'icons'); +const iconsDir = path.resolve( + getDirnameFromImportMeta(import.meta.url), + '..', + 'icons', +); + +/** + * @typedef {import('..').SimpleIcon} SimpleIcon + */ /** * Checks if icon data matches a subject icon. - * @param {import('..').SimpleIcon} icon Icon data - * @param {import('..').SimpleIcon} subject Icon to check against icon data + * @param {SimpleIcon} icon Icon data + * @param {SimpleIcon} subject Icon to check against icon data * @param {String} slug Icon data slug */ export const testIcon = (icon, subject, slug) => { @@ -38,7 +51,7 @@ export const testIcon = (icon, subject, slug) => { }); it('has a valid "path" value', () => { - assert.match(subject.path, /^[MmZzLlHhVvCcSsQqTtAaEe0-9-,.\s]+$/g); + assert.match(subject.path, SVG_PATH_REGEX); }); it(`has ${icon.guidelines ? 'the correct' : 'no'} "guidelines"`, () => { @@ -62,8 +75,8 @@ export const testIcon = (icon, subject, slug) => { } }); - it('has a valid svg value', () => { - const svgFileContents = fs.readFileSync(svgPath, 'utf8'); + it('has a valid svg value', async () => { + const svgFileContents = await fs.readFile(svgPath, 'utf8'); assert.equal(subject.svg, svgFileContents); });