convert scripts to esm (#6946)

* convert scripts to esm

* fix tests

* fix tests

* fix lints

* syncFs to fsSync

* named export for fs

Co-authored-by: LitoMore <LitoMore@users.noreply.github.com>

* fsSync to { promises as fs }

* convert update-svgs-count to esm

* rename data to icons

* fix build script

* switch svglintrc file to mjs

* use node: protocol

* pluralize getIcons

Co-authored-by: LitoMore <LitoMore@users.noreply.github.com>
This commit is contained in:
Sachin Raja 2021-12-25 06:22:56 -08:00 committed by GitHub
parent 0150cbd986
commit a930dc57ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 319 additions and 226 deletions

View File

@ -1,11 +1,28 @@
const fs = require('fs');
import fs from 'node:fs';
import path from 'node:path';
import {
getDirnameFromImportMeta,
htmlFriendlyToTitle,
} from './scripts/utils.js';
import svgpath from 'svgpath';
import svgPathBbox from 'svg-path-bbox';
import parsePath from 'svg-path-segments';
const data = require('./_data/simple-icons.json');
const { htmlFriendlyToTitle } = require('./scripts/utils.js');
const htmlNamedEntities = require('named-html-entities-json');
const svgpath = require('svgpath');
const svgPathBbox = require('svg-path-bbox');
const parsePath = require('svg-path-segments');
const __dirname = getDirnameFromImportMeta(import.meta.url);
const dataFile = path.join(__dirname, '_data', 'simple-icons.json');
const htmlNamedEntitiesFile = path.join(
__dirname,
'node_modules',
'named-html-entities-json',
'index.json',
);
const svglintIgnoredFile = path.join(__dirname, '.svglint-ignored.json');
const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
const htmlNamedEntities = JSON.parse(
fs.readFileSync(htmlNamedEntitiesFile, 'utf8'),
);
const svglintIgnores = JSON.parse(fs.readFileSync(svglintIgnoredFile, 'utf8'));
const svgRegexp =
/^<svg( [^\s]*=".*"){3}><title>.*<\/title><path d=".*"\/><\/svg>\n?$/;
@ -19,7 +36,7 @@ const iconTolerance = 0.001;
// set env SI_UPDATE_IGNORE to recreate the ignore file
const updateIgnoreFile = process.env.SI_UPDATE_IGNORE === 'true';
const ignoreFile = './.svglint-ignored.json';
const iconIgnored = !updateIgnoreFile ? require(ignoreFile) : {};
const iconIgnored = !updateIgnoreFile ? svglintIgnores : {};
const sortObjectByKey = (obj) => {
return Object.keys(obj)
@ -126,7 +143,7 @@ const ignoreIcon = (linterName, path, $) => {
iconIgnored[linterName][path] = iconName;
};
module.exports = {
export default {
rules: {
elm: {
svg: 1,

View File

@ -56,12 +56,12 @@
"our-lint": "node scripts/lint/ourlint.js",
"jslint": "prettier --check .",
"jsonlint": "node scripts/lint/jsonlint.js",
"svglint": "svglint icons/*.svg --ci",
"svglint": "svglint icons/*.svg --ci --config .svglintrc.mjs",
"wslint": "editorconfig-checker",
"prepare": "is-ci || husky install",
"prepublishOnly": "npm run build",
"postpublish": "npm run clean",
"test": "node --experimental-specifier-resolution=node node_modules/uvu/bin.js",
"test": "uvu",
"pretest": "npm run prepublishOnly",
"posttest": "npm run postpublish",
"svgo": "svgo --config svgo.config.js",

View File

@ -7,15 +7,24 @@
* tree-shakeable
*/
const fs = require('fs').promises;
const path = require('path');
const util = require('util');
const { transform: esbuildTransform } = require('esbuild');
import { promises as fs } from 'node:fs';
import path from 'node:path';
import util from 'node:util';
import { transform as esbuildTransform } from 'esbuild';
import {
getIconSlug,
svgToPath,
titleToHtmlFriendly,
slugToVariableName,
getIconsData,
getDirnameFromImportMeta,
} from '../utils.js';
const __dirname = getDirnameFromImportMeta(import.meta.url);
const UTF8 = 'utf8';
const rootDir = path.resolve(__dirname, '..', '..');
const dataFile = path.resolve(rootDir, '_data', 'simple-icons.json');
const indexFile = path.resolve(rootDir, 'index.js');
const iconsDir = path.resolve(rootDir, 'icons');
const iconsJsFile = path.resolve(rootDir, 'icons.js');
@ -26,15 +35,8 @@ const templatesDir = path.resolve(__dirname, 'templates');
const indexTemplateFile = path.resolve(templatesDir, 'index.js');
const iconObjectTemplateFile = path.resolve(templatesDir, 'icon-object.js');
const data = require(dataFile);
const {
getIconSlug,
svgToPath,
titleToHtmlFriendly,
slugToVariableName,
} = require('../utils.js');
const build = async () => {
const icons = await getIconsData();
const indexTemplate = await fs.readFile(indexTemplateFile, UTF8);
const iconObjectTemplate = await fs.readFile(iconObjectTemplateFile, UTF8);
@ -82,16 +84,16 @@ const build = async () => {
const iconsBarrelMjs = [];
const iconsBarrelJs = [];
const iconsBarrelDts = [];
const icons = [];
const buildIcons = [];
await Promise.all(
data.icons.map(async (icon) => {
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.path = svgToPath(icon.svg);
icon.slug = filename;
icons.push(icon);
buildIcons.push(icon);
const iconObject = iconToObject(icon);
@ -126,7 +128,7 @@ const build = async () => {
// write our generic index.js
const rawIndexJs = util.format(
indexTemplate,
icons.map(iconToKeyValue).join(','),
buildIcons.map(iconToKeyValue).join(','),
);
await writeJs(indexFile, rawIndexJs);

View File

@ -5,7 +5,7 @@
* icon SVG filename to standard output.
*/
const { titleToSlug } = require('./utils.js');
import { titleToSlug } from './utils.js';
if (process.argv.length < 3) {
console.error('Provide a brand name as argument');

View File

@ -4,23 +4,30 @@
* CLI tool to run jsonschema on the simple-icons.json data file.
*/
const path = require('path');
const { Validator } = require('jsonschema');
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { Validator } from 'jsonschema';
import { getDirnameFromImportMeta, getIconsData } from '../utils.js';
const __dirname = getDirnameFromImportMeta(import.meta.url);
const rootDir = path.resolve(__dirname, '..', '..');
const schemaFile = path.resolve(rootDir, '.jsonschema.json');
const dataFile = path.resolve(rootDir, '_data', 'simple-icons.json');
const schema = require(schemaFile);
const data = require(dataFile);
(async () => {
const icons = await getIconsData();
const schema = JSON.parse(await fs.readFile(schemaFile, 'utf8'));
const validator = new Validator();
const result = validator.validate(data, schema);
if (result.errors.length > 0) {
result.errors.forEach((error) => {
console.error(error);
});
const validator = new Validator();
const result = validator.validate({ icons }, schema);
if (result.errors.length > 0) {
result.errors.forEach((error) => {
console.error(error);
});
console.error(`Found ${result.errors.length} error(s) in simple-icons.json`);
process.exit(1);
}
console.error(
`Found ${result.errors.length} error(s) in simple-icons.json`,
);
process.exit(1);
}
})();

View File

@ -5,16 +5,8 @@
* linters (e.g. jsonlint/svglint).
*/
const fs = require('fs');
const path = require('path');
const fakeDiff = require('fake-diff');
const UTF8 = 'utf8';
const rootDir = path.resolve(__dirname, '..', '..');
const dataFile = path.resolve(rootDir, '_data', 'simple-icons.json');
const data = require(dataFile);
import fakeDiff from 'fake-diff';
import { getIconsDataString } from '../utils.js';
/**
* Contains our tests so they can be isolated from each other.
@ -22,7 +14,7 @@ const data = require(dataFile);
*/
const TESTS = {
/* Tests whether our icons are in alphabetical order */
alphabetical: () => {
alphabetical: (data) => {
const collector = (invalidEntries, icon, index, array) => {
if (index > 0) {
const prev = array[index - 1];
@ -54,22 +46,30 @@ const TESTS = {
},
/* Check the formatting of the data file */
prettified: () => {
const dataString = fs.readFileSync(dataFile, UTF8).replace(/\r\n/g, '\n');
prettified: async (data, dataString) => {
const normalizedDataString = dataString.replace(/\r\n/g, '\n');
const dataPretty = `${JSON.stringify(data, null, ' ')}\n`;
if (dataString !== dataPretty) {
const dataDiff = fakeDiff(dataString, dataPretty);
if (normalizedDataString !== dataPretty) {
const dataDiff = fakeDiff(normalizedDataString, dataPretty);
return `Data file is formatted incorrectly:\n\n${dataDiff}`;
}
},
};
// execute all tests and log all errors
const errors = Object.keys(TESTS)
.map((k) => TESTS[k]())
.filter(Boolean);
(async () => {
const dataString = await getIconsDataString();
const data = JSON.parse(dataString);
if (errors.length > 0) {
errors.forEach((error) => console.error(`\u001b[31m${error}\u001b[0m`));
process.exit(1);
}
const errors = (
await Promise.all(
Object.keys(TESTS).map((test) => TESTS[test](data, dataString)),
)
).filter(Boolean);
if (errors.length > 0) {
errors.forEach((error) => console.error(`\u001b[31m${error}\u001b[0m`));
process.exit(1);
}
})();

3
scripts/package.json Normal file
View File

@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@ -4,8 +4,11 @@
* Updates the version of this package to the CLI specified version.
*/
const fs = require('fs');
const path = require('path');
import fs from 'node:fs';
import path from 'node:path';
import { getDirnameFromImportMeta } from '../utils.js';
const __dirname = getDirnameFromImportMeta(import.meta.url);
const rootDir = path.resolve(__dirname, '..', '..');
const packageJsonFile = path.resolve(rootDir, 'package.json');

View File

@ -5,8 +5,11 @@
* NPM package manifest. Does nothing if the README.md is already up-to-date.
*/
const fs = require('fs');
const path = require('path');
import fs from 'node:fs';
import path from 'node:path';
import { getDirnameFromImportMeta } from '../utils.js';
const __dirname = getDirnameFromImportMeta(import.meta.url);
const rootDir = path.resolve(__dirname, '..', '..');
const packageJsonFile = path.resolve(rootDir, 'package.json');

View File

@ -4,16 +4,17 @@
* Generates a MarkDown file that lists every brand name and their slug.
*/
const fs = require('fs');
const path = require('path');
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getIconsData, getIconSlug } from '../utils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..', '..');
const dataFile = path.resolve(rootDir, '_data', 'simple-icons.json');
const slugsFile = path.resolve(rootDir, 'slugs.md');
const data = require(dataFile);
const { getIconSlug } = require('../utils.js');
let content = `<!--
This file is automatically generated. If you want to change something, please
update the script at '${path.relative(rootDir, __filename)}'.
@ -25,10 +26,14 @@ update the script at '${path.relative(rootDir, __filename)}'.
| :--- | :--- |
`;
data.icons.forEach((icon) => {
const brandName = icon.title;
const brandSlug = getIconSlug(icon);
content += `| \`${brandName}\` | \`${brandSlug}\` |\n`;
});
(async () => {
const icons = await getIconsData();
fs.writeFileSync(slugsFile, content);
icons.forEach((icon) => {
const brandName = icon.title;
const brandSlug = getIconSlug(icon);
content += `| \`${brandName}\` | \`${brandSlug}\` |\n`;
});
await fs.writeFile(slugsFile, content);
})();

View File

@ -5,34 +5,39 @@
* at README every time the number of current icons is more than `updateRange`
* more than the previous milestone.
*/
const fs = require('fs');
const path = require('path');
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { getDirnameFromImportMeta, getIconsData } from '../utils.js';
const regexMatcher = /Over\s(\d+)\s/;
const updateRange = 100;
const __dirname = getDirnameFromImportMeta(import.meta.url);
const rootDir = path.resolve(__dirname, '..', '..');
const dataFile = path.resolve(rootDir, '_data', 'simple-icons.json');
const readmeFile = path.resolve(rootDir, 'README.md');
const readmeContent = fs.readFileSync(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);
}
(async () => {
const readmeContent = await fs.readFile(readmeFile, 'utf-8');
const nIcons = require(dataFile).icons.length,
newNIcons = overNIconsInReadme + updateRange;
if (nIcons <= newNIcons) {
process.exit(0);
}
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 newContent = readmeContent.replace(regexMatcher, `Over ${newNIcons} `);
fs.writeFileSync(readmeFile, newContent);
const nIcons = (await getIconsData()).length;
const newNIcons = overNIconsInReadme + updateRange;
if (nIcons <= newNIcons) {
process.exit(0);
}
const newContent = readmeContent.replace(regexMatcher, `Over ${newNIcons} `);
await fs.writeFile(readmeFile, newContent);
})();

View File

@ -3,76 +3,103 @@
* Some common utilities for scripts.
*/
module.exports = {
/**
* Get the slug/filename for an icon.
* @param {Object} icon The icon data as it appears in _data/simple-icons.json
*/
getIconSlug: (icon) => icon.slug || module.exports.titleToSlug(icon.title),
import path from 'node:path';
import { promises as fs } from 'node:fs';
import { fileURLToPath } from 'node:url';
/**
* Extract the path from an icon SVG content.
* @param {Object} svg The icon SVG content
**/
svgToPath: (svg) => svg.match(/<path\s+d="([^"]*)/)[1],
/**
* Get the slug/filename for an icon.
* @param {Object} icon The icon data as it appears in _data/simple-icons.json
*/
export const getIconSlug = (icon) => icon.slug || titleToSlug(icon.title);
/**
* Converts a brand title into a slug/filename.
* @param {String} title The title to convert
*/
titleToSlug: (title) =>
title
.toLowerCase()
.replace(/\+/g, 'plus')
.replace(/\./g, 'dot')
.replace(/&/g, 'and')
.replace(/đ/g, 'd')
.replace(/ħ/g, 'h')
.replace(/ı/g, 'i')
.replace(/ĸ/g, 'k')
.replace(/ŀ/g, 'l')
.replace(/ł/g, 'l')
.replace(/ß/g, 'ss')
.replace(/ŧ/g, 't')
.normalize('NFD')
.replace(/[^a-z0-9]/g, ''),
/**
* Extract the path from an icon SVG content.
* @param {Object} svg The icon SVG content
**/
export const svgToPath = (svg) => svg.match(/<path\s+d="([^"]*)/)[1];
/**
* Converts a brand title in HTML/SVG friendly format into a brand title (as
* it is seen in simple-icons.json)
* @param {String} htmlFriendlyTitle The title to convert
*/
htmlFriendlyToTitle: (htmlFriendlyTitle) =>
htmlFriendlyTitle
.replace(/&#([0-9]+);/g, (_, num) => String.fromCharCode(parseInt(num)))
.replace(
/&(quot|amp|lt|gt);/g,
(_, ref) => ({ quot: '"', amp: '&', lt: '<', gt: '>' }[ref]),
),
/**
* Converts a brand title into a slug/filename.
* @param {String} title The title to convert
*/
export const titleToSlug = (title) =>
title
.toLowerCase()
.replace(/\+/g, 'plus')
.replace(/\./g, 'dot')
.replace(/&/g, 'and')
.replace(/đ/g, 'd')
.replace(/ħ/g, 'h')
.replace(/ı/g, 'i')
.replace(/ĸ/g, 'k')
.replace(/ŀ/g, 'l')
.replace(/ł/g, 'l')
.replace(/ß/g, 'ss')
.replace(/ŧ/g, 't')
.normalize('NFD')
.replace(/[^a-z0-9]/g, '');
/**
* Converts a slug into a variable name that can be exported.
* @param {String} slug The slug to convert
*/
slugToVariableName: (slug) => {
const slugFirstLetter = slug[0].toUpperCase();
const slugRest = slug.slice(1);
return `si${slugFirstLetter}${slugRest}`;
},
/**
* Converts a brand title (as it is seen in simple-icons.json) into a brand
* title in HTML/SVG friendly format.
* @param {String} brandTitle The title to convert
*/
titleToHtmlFriendly: (brandTitle) =>
brandTitle
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/./g, (char) => {
const charCode = char.charCodeAt(0);
return charCode > 127 ? `&#${charCode};` : char;
}),
/**
* Converts a slug into a variable name that can be exported.
* @param {String} slug The slug to convert
*/
export const slugToVariableName = (slug) => {
const slugFirstLetter = slug[0].toUpperCase();
const slugRest = slug.slice(1);
return `si${slugFirstLetter}${slugRest}`;
};
/**
* Converts a brand title (as it is seen in simple-icons.json) into a brand
* title in HTML/SVG friendly format.
* @param {String} brandTitle The title to convert
*/
export const titleToHtmlFriendly = (brandTitle) =>
brandTitle
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/./g, (char) => {
const charCode = char.charCodeAt(0);
return charCode > 127 ? `&#${charCode};` : char;
});
/**
* Converts a brand title in HTML/SVG friendly format into a brand title (as
* it is seen in simple-icons.json)
* @param {String} htmlFriendlyTitle The title to convert
*/
export const htmlFriendlyToTitle = (htmlFriendlyTitle) =>
htmlFriendlyTitle
.replace(/&#([0-9]+);/g, (_, num) => String.fromCharCode(parseInt(num)))
.replace(
/&(quot|amp|lt|gt);/g,
(_, ref) => ({ quot: '"', amp: '&', lt: '<', gt: '>' }[ref]),
);
/**
* Get contents of _data/simple-icons.json.
*/
export const getIconsDataString = () => {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(__dirname, '..');
const iconDataPath = path.resolve(rootDir, '_data', 'simple-icons.json');
return fs.readFile(iconDataPath, 'utf8');
};
/**
* Get icon data as object from _data/simple-icons.json.
*/
export const getIconsData = async () => {
const fileContents = await getIconsDataString();
return JSON.parse(fileContents).icons;
};
/**
* Get the directory name from import.meta.url.
* @param {String} importMetaUrl import.meta.url
*/
export const getDirnameFromImportMeta = (importMetaUrl) =>
path.dirname(fileURLToPath(importMetaUrl));

19
tests/icons-cjs.test.js Normal file
View File

@ -0,0 +1,19 @@
import { exec } from 'uvu';
import { testIcon } from './test-icon.js';
import { getIconSlug, getIconsData } from '../scripts/utils.js';
(async () => {
console.warn = () => {};
const icons = await getIconsData();
const tests = icons.map(async (icon) => {
const slug = getIconSlug(icon);
const { default: subject } = await import(`../icons/${slug}.js`);
testIcon(icon, subject, slug);
});
await Promise.all(tests);
exec();
})();

22
tests/icons-esm.test.js Normal file
View File

@ -0,0 +1,22 @@
import {
getIconsData,
getIconSlug,
slugToVariableName,
} from '../scripts/utils.js';
import * as simpleIcons from '../icons.mjs';
import { testIcon } from './test-icon.js';
import { exec } from 'uvu';
(async () => {
const icons = await getIconsData();
icons.map((icon) => {
const slug = getIconSlug(icon);
const variableName = slugToVariableName(slug);
const subject = simpleIcons[variableName];
testIcon(icon, subject, slug);
});
exec();
})();

View File

@ -1,12 +0,0 @@
const { icons } = require('../_data/simple-icons.json');
const { getIconSlug } = require('../scripts/utils.js');
const testIcon = require('./test-icon.js');
console.warn = () => {};
icons.forEach((icon) => {
const slug = getIconSlug(icon);
const subject = require(`../icons/${slug}.js`);
testIcon(icon, subject, slug);
});

View File

@ -1,14 +0,0 @@
import simpleIconsData from '../_data/simple-icons.json';
import utils from '../scripts/utils.js';
import * as simpleIcons from '../icons.mjs';
import testIcon from './test-icon.js';
const { getIconSlug, slugToVariableName } = utils;
simpleIconsData.icons.forEach((icon) => {
const slug = getIconSlug(icon);
const variableName = slugToVariableName(slug);
const subject = simpleIcons[variableName];
testIcon(icon, subject, slug);
});

View File

@ -1,27 +1,32 @@
const { icons } = require('../_data/simple-icons.json');
const simpleIcons = require('../index.js');
const { getIconSlug } = require('../scripts/utils');
const { test } = require('uvu');
const assert = require('uvu/assert');
import simpleIcons from '../index.js';
import { getIconSlug, getIconsData } from '../scripts/utils.js';
import { test, exec } from 'uvu';
import * as assert from 'uvu/assert';
icons.forEach((icon) => {
const slug = getIconSlug(icon);
(async () => {
const icons = await getIconsData();
test(`'Get' ${icon.title} by its slug`, () => {
const found = simpleIcons.Get(slug);
assert.ok(found);
assert.is(found.title, icon.title);
assert.is(found.hex, icon.hex);
assert.is(found.source, icon.source);
icons.forEach((icon) => {
const slug = getIconSlug(icon);
test(`'Get' ${icon.title} by its slug`, () => {
const found = simpleIcons.Get(slug);
assert.ok(found);
assert.is(found.title, icon.title);
assert.is(found.hex, icon.hex);
assert.is(found.source, icon.source);
});
});
});
test(`Iterating over simpleIcons only exposes icons`, () => {
const iconArray = Object.values(simpleIcons);
for (let icon of iconArray) {
assert.ok(icon);
assert.type(icon, 'object');
}
});
test(`Iterating over simpleIcons only exposes icons`, () => {
const iconArray = Object.values(simpleIcons);
for (let icon of iconArray) {
assert.ok(icon);
assert.type(icon, 'object');
}
});
test.run();
test.run();
exec();
})();

3
tests/package.json Normal file
View File

@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@ -1,9 +1,9 @@
const fs = require('fs');
const path = require('path');
import fs from 'node:fs';
import path from 'node:path';
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
const iconsDir = path.resolve(process.cwd(), 'icons');
const { suite } = require('uvu');
const assert = require('uvu/assert');
/**
* Checks if icon data matches a subject icon.
@ -11,7 +11,7 @@ const assert = require('uvu/assert');
* @param {import('..').SimpleIcon} subject Icon to check against icon data
* @param {String} slug Icon data slug
*/
const testIcon = (icon, subject, slug) => {
export const testIcon = (icon, subject, slug) => {
const test = suite(icon.title);
const svgPath = path.resolve(iconsDir, `${slug}.svg`);
@ -69,5 +69,3 @@ const testIcon = (icon, subject, slug) => {
test.run();
};
module.exports = testIcon;