Refactor tests and scripts (#9237)

Co-authored-by: LitoMore <LitoMore@users.noreply.github.com>
This commit is contained in:
Álvaro Mondéjar 2023-08-07 22:38:52 -06:00 committed by GitHub
parent 8abcd9c8b9
commit 17ea889273
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 138 additions and 129 deletions

View file

@ -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 =
/^<svg( [^\s]*=".*"){3}><title>.*<\/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,
'',
),

View file

@ -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",

View file

@ -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);

View file

@ -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='</title><path d="',c='"/></svg>';`;

View file

@ -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) {

View file

@ -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);
}

View file

@ -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 errors = (
await Promise.all(
Object.keys(TESTS).map((test) => TESTS[test](data, dataString)),
)
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);
}
})();

View file

@ -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();

View file

@ -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 */';

View file

@ -25,14 +25,10 @@ update the script at '${path.relative(rootDir, __filename)}'.
| :--- | :--- |
`;
(async () => {
const icons = await getIconsData();
icons.forEach((icon) => {
for (const icon of icons) {
const brandName = icon.title;
const brandSlug = getIconSlug(icon);
content += `| \`${brandName}\` | \`${brandSlug}\` |\n`;
});
}
await fs.writeFile(slugsFile, content);
})();

View file

@ -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,11 +14,9 @@ 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');
let overNIconsInReadme;
@ -33,10 +33,7 @@ const readmeFile = path.resolve(rootDir, 'README.md');
const nIcons = (await getIconsData()).length;
const newNIcons = overNIconsInReadme + updateRange;
if (nIcons <= newNIcons) {
process.exit(0);
}
if (nIcons > newNIcons) {
const newContent = readmeContent.replace(regexMatcher, `Over ${newNIcons} `);
await fs.writeFile(readmeFile, newContent);
})();
}

View file

@ -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',

1
sdk.d.ts vendored
View file

@ -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;

16
sdk.mjs
View file

@ -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(/<path\s+d="([^"]*)/)[1];
export const svgToPath = (svg) => 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('<img src="')[0];
return {
module: {

View file

@ -1,15 +1,22 @@
import fs from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
import { describe, test } from 'mocha';
import { test } from 'mocha';
import { strict as assert } from 'node:assert';
import { getThirdPartyExtensions, getDirnameFromImportMeta } from '../sdk.mjs';
import {
getThirdPartyExtensions,
getDirnameFromImportMeta,
URL_REGEX,
} from '../sdk.mjs';
const __dirname = getDirnameFromImportMeta(import.meta.url);
const root = path.dirname(__dirname);
const getLinksRegex = new RegExp(
URL_REGEX.source.replace('^https', 'https?'),
'gm',
);
test('README third party extensions must be alphabetically sorted', async () => {
const readmePath = path.join(root, 'README.md');
const thirdPartyExtensions = await getThirdPartyExtensions(readmePath);
const thirdPartyExtensions = await getThirdPartyExtensions();
assert.ok(thirdPartyExtensions.length > 0);
const thirdPartyExtensionsNames = thirdPartyExtensions.map(
@ -27,22 +34,21 @@ test('README third party extensions must be alphabetically sorted', async () =>
test('Only allow HTTPS links in documentation pages', async () => {
const ignoreHttpLinks = ['http://www.w3.org/2000/svg'];
const docsFiles = fs
.readdirSync(root)
.filter((fname) => fname.endsWith('.md'));
const docsFiles = (await fs.readdir(root)).filter((fname) =>
fname.endsWith('.md'),
);
const linksGetter = new RegExp('http://[^\\s"\']+', 'g');
for (let docsFile of docsFiles) {
for (const docsFile of docsFiles) {
const docsFilePath = path.join(root, docsFile);
const docsFileContent = fs.readFileSync(docsFilePath, 'utf8');
const docsFileContent = await fs.readFile(docsFilePath, 'utf8');
Array.from(docsFileContent.matchAll(linksGetter)).forEach((match) => {
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.`,
);
});
}
}
});

View file

@ -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();
icons.map((icon) => {
for (const icon of await getIconsData()) {
const slug = getIconSlug(icon);
const variableName = slugToVariableName(slug);
const subject = simpleIcons[variableName];
testIcon(icon, subject, slug);
});
})();
}

View file

@ -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);
});