From 236f5fc715fe74ff42861199d6ce9216b272e7f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Mond=C3=A9jar=20Rubio?= Date: Thu, 6 Jun 2024 14:40:35 +0200 Subject: [PATCH] Add types to source code (#10637) --- .npmignore | 1 + .xo-config.json | 10 +- jsconfig.json | 13 ++ package.json | 6 +- scripts/add-icon-data.js | 35 +++- scripts/build/clean.js | 2 +- scripts/build/package.js | 119 +++++++----- scripts/get-filename.js | 2 +- scripts/lint/jsonlint.js | 2 +- scripts/lint/ourlint.js | 54 +++++- scripts/release/reformat-markdown.js | 6 +- scripts/release/update-cdn-urls.js | 13 +- scripts/release/update-sdk-ts-defs.js | 56 +++++- scripts/release/update-slugs-table.js | 2 +- scripts/release/update-svgs-count.js | 36 ++-- scripts/utils.js | 18 +- sdk.d.ts | 9 +- sdk.mjs | 133 +++++++++----- svglint.config.mjs | 254 ++++++++++++++++++++------ svgo.config.mjs | 8 +- tests/docs.test.js | 4 + tests/index.test.js | 8 + tests/min-reporter.cjs | 10 + tests/test-icon.js | 18 +- types.d.ts | 5 +- 25 files changed, 619 insertions(+), 205 deletions(-) create mode 100644 jsconfig.json diff --git a/.npmignore b/.npmignore index 65b188954..fee6e22df 100644 --- a/.npmignore +++ b/.npmignore @@ -16,3 +16,4 @@ !sdk.js !sdk.d.ts !.jsonschema.json +!jsconfig.json diff --git a/.xo-config.json b/.xo-config.json index e30f926f3..49d31dd8a 100644 --- a/.xo-config.json +++ b/.xo-config.json @@ -2,6 +2,7 @@ "prettier": true, "space": 2, "plugins": ["import"], + "extends": ["plugin:jsdoc/recommended"], "rules": { "sort-imports": [ "error", @@ -27,7 +28,8 @@ "newlines-between": "never" } ], - "no-console": ["error", {"allow": ["warn", "error"]}] + "no-console": ["error", {"allow": ["warn", "error"]}], + "jsdoc/require-file-overview": "error" }, "overrides": [ { @@ -46,6 +48,12 @@ "svgo.config.mjs" ], "nodeVersion": ">=18" + }, + { + "files": ["svglint.config.mjs"], + "rules": { + "max-depth": "off" + } } ] } diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 000000000..ce221ee02 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "node16", + "moduleResolution": "node16", + "checkJs": true, + "skipLibCheck": false, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/package.json b/package.json index 4ebcdeecf..1414bad2f 100644 --- a/package.json +++ b/package.json @@ -86,10 +86,12 @@ "devDependencies": { "@inquirer/core": "8.1.0", "@inquirer/prompts": "5.0.2", + "@types/node": "20.14.2", "chalk": "5.3.0", "editorconfig-checker": "5.1.5", "esbuild": "0.20.2", "eslint-plugin-import": "2.29.1", + "eslint-plugin-jsdoc": "48.2.8", "fake-diff": "1.0.0", "fast-fuzzy": "1.12.0", "get-relative-luminance": "1.0.0", @@ -99,8 +101,8 @@ "markdown-link-check": "3.12.1", "mocha": "10.4.0", "named-html-entities-json": "1.0.0", - "svg-path-bbox": "1.2.6", - "svg-path-segments": "2.0.0", + "svg-path-bbox": "2.0.0", + "svg-path-segments": "2.0.1", "svglint": "2.7.1", "svgo": "3.2.0", "svgpath": "2.6.0", diff --git a/scripts/add-icon-data.js b/scripts/add-icon-data.js index 9ee694d31..2719bcdd8 100644 --- a/scripts/add-icon-data.js +++ b/scripts/add-icon-data.js @@ -1,4 +1,12 @@ #!/usr/bin/env node +/** + * @file + * Script to add data for a new icon to the simple-icons dataset. + */ + +/** + * @typedef {import("../sdk.js").IconData} IconData + */ import process from 'node:process'; import {ExitPromptError} from '@inquirer/core'; import {checkbox, confirm, input} from '@inquirer/prompts'; @@ -15,6 +23,7 @@ import { } from '../sdk.mjs'; import {getJsonSchemaData, writeIconsData} from './utils.js'; +/** @type {{icons: import('../sdk.js').IconData[]}} */ const iconsData = JSON.parse(await getIconsDataString()); const jsonSchema = await getJsonSchemaData(); @@ -25,25 +34,42 @@ const aliasTypes = ['aka', 'old'].map((key) => ({ value: key, })); +/** @type {{name: string, value: string}[]} */ const licenseTypes = jsonSchema.definitions.brand.properties.license.oneOf[0].properties.type.enum.map( - (license) => ({name: license, value: license}), + (/** @type {string} */ license) => ({name: license, value: license}), ); +/** + * @param {string} input URL input + * @returns {Promise} 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.'; }; +/** + * @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.'; +/** + * @param {string} input New icon input + * @returns {boolean} Whether the icon is new + */ const isNewIcon = (input) => !iconsData.icons.some( (icon) => icon.title === input || titleToSlug(icon.title) === titleToSlug(input), - ) || 'This icon title or slug already exists.'; + ); +/** + * @param {string} input Color input + * @returns {string} Preview of the color + */ const previewHexColor = (input) => { const color = normalizeColor(input); const luminance = HEX_REGEX.test(input) @@ -60,7 +86,9 @@ try { title: await input({ message: 'What is the title of this icon?', validate: (input) => - input.trim().length > 0 ? isNewIcon(input) : 'This field is required.', + input.trim().length > 0 + ? isNewIcon(input) || 'This icon title or slug already exists.' + : 'This field is required.', }), hex: normalizeColor( await input({ @@ -111,6 +139,7 @@ try { }).then(async (aliases) => { const result = {}; for (const alias of aliases) { + // @ts-ignore // eslint-disable-next-line no-await-in-loop result[alias] = await input({ message: `What ${alias} aliases would you like to add? (separate with commas)`, diff --git a/scripts/build/clean.js b/scripts/build/clean.js index 83423f25d..ada054674 100644 --- a/scripts/build/clean.js +++ b/scripts/build/clean.js @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * @fileoverview + * @file * Clean files built by the build process. */ diff --git a/scripts/build/package.js b/scripts/build/package.js index edf737682..2fa563da1 100644 --- a/scripts/build/package.js +++ b/scripts/build/package.js @@ -1,9 +1,14 @@ #!/usr/bin/env node /** - * @fileoverview + * @file * Simple Icons package build script. */ +/** + * @typedef {import('../../types.js').License} License + * @typedef {import('esbuild').TransformOptions} EsBuildTransformOptions + */ + import {promises as fs} from 'node:fs'; import path from 'node:path'; import util from 'node:util'; @@ -36,62 +41,78 @@ const iconObjectTemplateFile = path.resolve( 'icon-object.js.template', ); +const icons = await getIconsData(); +const iconObjectTemplate = await fs.readFile(iconObjectTemplateFile, UTF8); + +/** + * @param {string} value The value to escape + * @returns {string} The escaped value + */ +const escape = (value) => { + return value.replaceAll(/(? { + if (license.url === undefined) { + license.url = `https://spdx.org/licenses/${license.type}`; + } + + return license; +}; + +// TODO: Find a way to type this object without decreasing performance +// @ts-ignore +const iconToJsObject = (icon) => { + return util.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(licenseToObject(icon.license))},`, + ); +}; + +/** + * @param {string} filepath The path to the file to write + * @param {string} rawJavaScript The raw JavaScript content to write to the file + * @param {EsBuildTransformOptions | null} options The options to pass to esbuild + */ +const writeJs = async (filepath, rawJavaScript, options = null) => { + options = options === null ? {minify: true} : options; + const {code} = await esbuildTransform(rawJavaScript, options); + await fs.writeFile(filepath, code); +}; + +/** + * @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); +}; + const build = async () => { - const icons = await getIconsData(); - const iconObjectTemplate = await fs.readFile(iconObjectTemplateFile, UTF8); - - // Local helper functions - const escape = (value) => { - return value.replaceAll(/(? { - if (license === undefined) { - return; - } - - if (license.url === undefined) { - license.url = `https://spdx.org/licenses/${license.type}`; - } - - return license; - }; - - const iconToObject = (icon) => { - return util.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)}',` : '', - licenseToObject(icon.license) - ? `\n license: ${JSON.stringify(licenseToObject(icon.license))},` - : '', - ); - }; - - const writeJs = async (filepath, rawJavaScript, options = null) => { - options = options === null ? {minify: true} : options; - const {code} = await esbuildTransform(rawJavaScript, options); - await fs.writeFile(filepath, code); - }; - - const writeTs = async (filepath, rawTypeScript) => { - await fs.writeFile(filepath, rawTypeScript); - }; - - // 'main' const buildIcons = await Promise.all( icons.map(async (icon) => { const filename = getIconSlug(icon); const svgFilepath = path.resolve(iconsDirectory, `${filename}.svg`); + // TODO: Find a way to type these objects without decreasing performance + // @ts-ignore icon.svg = await fs.readFile(svgFilepath, UTF8); + // @ts-ignore icon.path = svgToPath(icon.svg); icon.slug = filename; - const iconObject = iconToObject(icon); + const iconObject = iconToJsObject(icon); const iconExportName = slugToVariableName(icon.slug); return {icon, iconObject, iconExportName}; }), diff --git a/scripts/get-filename.js b/scripts/get-filename.js index e20f4c123..c55fea2dc 100644 --- a/scripts/get-filename.js +++ b/scripts/get-filename.js @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * @fileoverview + * @file * Script that takes a brand name as argument and outputs the corresponding * icon SVG filename to standard output. */ diff --git a/scripts/lint/jsonlint.js b/scripts/lint/jsonlint.js index a439bd732..623134256 100644 --- a/scripts/lint/jsonlint.js +++ b/scripts/lint/jsonlint.js @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * @fileoverview + * @file * CLI tool to run jsonschema on the simple-icons.json data file. */ diff --git a/scripts/lint/ourlint.js b/scripts/lint/ourlint.js index fcb320498..b76b90b67 100644 --- a/scripts/lint/ourlint.js +++ b/scripts/lint/ourlint.js @@ -1,21 +1,39 @@ #!/usr/bin/env node /** - * @fileoverview + * @file * Linters for the package that can't easily be implemented in the existing * linters (e.g. jsonlint/svglint). */ +/** + * @typedef {import("../../sdk.mjs").IconData} IconData + * @typedef {import("../../types.js").CustomLicense} CustomLicense + * @typedef {IconData[]} IconsData + */ + import process from 'node:process'; import fakeDiff from 'fake-diff'; import {collator, getIconsDataString, normalizeNewlines} from '../../sdk.mjs'; /** * Contains our tests so they can be isolated from each other. - * @type {{[k:string]: () => (string|undefined)}} + * @type {{[k: string]: (arg0: {icons: IconsData}, arg1: string) => string | undefined}} */ const TESTS = { - /* Tests whether our icons are in alphabetical order */ + /** + * Tests whether our icons are in alphabetical order + * @param {{icons: IconsData}} data Icons data + * @returns {string|undefined} Error message or undefined + */ alphabetical(data) { + /** + * 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]; @@ -34,6 +52,11 @@ const TESTS = { 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})`; @@ -63,6 +86,11 @@ const TESTS = { /* Check redundant trailing slash in URL */ checkUrl(data) { + /** + * Check if an URL has a redundant trailing slash. + * @param {string} url URL to check + * @returns {boolean} Whether the URL has a redundant trailing slash + */ const hasRedundantTrailingSlash = (url) => { const {origin} = new global.URL(url); return /^\/+$/.test(url.replace(origin, '')); @@ -71,7 +99,17 @@ const TESTS = { const allUrlFields = [ ...new Set( data.icons - .flatMap((icon) => [icon.source, icon.guidelines, icon.license?.url]) + .flatMap((icon) => { + // TODO: `Omit` is not working smoothly here + const license = + // @ts-ignore + icon.license && icon.license.url + ? // @ts-ignore + [icon.license.url] + : []; + return [icon.source, icon.guidelines, ...license]; + }) + .filter(Boolean), ), ]; @@ -88,11 +126,13 @@ const TESTS = { }, }; -const dataString = await getIconsDataString(); -const data = JSON.parse(dataString); +const iconsDataString = await getIconsDataString(); +const iconsData = JSON.parse(iconsDataString); const errors = ( - await Promise.all(Object.values(TESTS).map((test) => test(data, dataString))) + await Promise.all( + Object.values(TESTS).map((test) => test(iconsData, iconsDataString)), + ) ) // eslint-disable-next-line unicorn/no-await-expression-member .filter(Boolean); diff --git a/scripts/release/reformat-markdown.js b/scripts/release/reformat-markdown.js index 8cbc9196d..657f4220e 100644 --- a/scripts/release/reformat-markdown.js +++ b/scripts/release/reformat-markdown.js @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * @fileoverview + * @file * Rewrite some Markdown files. */ @@ -17,6 +17,10 @@ const rootDirectory = path.resolve(__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( diff --git a/scripts/release/update-cdn-urls.js b/scripts/release/update-cdn-urls.js index 38a1f76ea..d436deb78 100644 --- a/scripts/release/update-cdn-urls.js +++ b/scripts/release/update-cdn-urls.js @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * @fileoverview + * @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. */ @@ -16,16 +16,27 @@ const rootDirectory = path.resolve(__dirname, '..', '..'); const packageJsonFile = path.resolve(rootDirectory, 'package.json'); const readmeFile = path.resolve(rootDirectory, 'README.md'); +/** + * @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); }; +/** + * @param {number} majorVersion The major version number. + */ const updateVersionInReadmeIfNecessary = async (majorVersion) => { let content = await fs.readFile(readmeFile, 'utf8'); diff --git a/scripts/release/update-sdk-ts-defs.js b/scripts/release/update-sdk-ts-defs.js index 9856c31e8..049d2f95b 100644 --- a/scripts/release/update-sdk-ts-defs.js +++ b/scripts/release/update-sdk-ts-defs.js @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * @fileoverview + * @file * Updates the SDK Typescript definitions located in the file sdk.d.ts * to match the current definitions of functions of sdk.mjs. */ @@ -34,19 +34,53 @@ const generateSdkMts = async () => { 'npx tsc sdk.mjs' + ' --declaration --emitDeclarationOnly --allowJs --removeComments', ); - } catch (error) { + } catch (/** @type {unknown} */ error) { await fs.writeFile(sdkMjs, originalSdkMjsContent); + + let errorMessage = error; + if (error instanceof Error) { + // The `execSync` function throws a generic Node.js Error + errorMessage = error.message; + } + process.stdout.write( - `Error ${error.status} generating Typescript` + - ` definitions: '${error.message}'` + - '\n', + `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 fileExists = await fs .access(sdkMts) @@ -62,15 +96,21 @@ const generateSdkTs = async () => { (await fs.readFile(sdkTs, 'utf8')).split(autogeneratedMessage)[0] + `${autogeneratedMessage}\n\n${await fs.readFile(sdkMts, 'utf8')}`; - await fs.writeFile(sdkTs, newSdkTsContent); + await fs.writeFile(sdkTs, removeDuplicatedExportTypes(newSdkTsContent)); await fs.unlink(sdkMts); try { execSync('npx prettier -w sdk.d.ts'); } catch (error) { + let errorMessage = error; + if (error instanceof Error) { + // The `execSync` function throws a generic Node.js Error + errorMessage = error.message; + } + process.stdout.write( - `Error ${error.status} executing Prettier` + - ` to prettify SDK TS definitions: '${error.message}'` + + 'Error executing Prettier to prettify' + + ` SDK TS definitions: '${errorMessage}'` + '\n', ); process.exit(1); diff --git a/scripts/release/update-slugs-table.js b/scripts/release/update-slugs-table.js index 74f61dbaa..7b3734bfa 100644 --- a/scripts/release/update-slugs-table.js +++ b/scripts/release/update-slugs-table.js @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * @fileoverview + * @file * Generates a MarkDown file that lists every brand name and their slug. */ diff --git a/scripts/release/update-svgs-count.js b/scripts/release/update-svgs-count.js index b90f7af1c..5ce295237 100644 --- a/scripts/release/update-svgs-count.js +++ b/scripts/release/update-svgs-count.js @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * @fileoverview + * @file * Replaces the SVG count milestone "Over Free SVG icons..." located * at README every time the number of current icons is more than `updateRange` * more than the previous milestone. @@ -20,22 +20,32 @@ const readmeFile = path.resolve(rootDirectory, 'README.md'); const readmeContent = await fs.readFile(readmeFile, 'utf8'); -let overNIconsInReadme; try { - overNIconsInReadme = Number.parseInt(regexMatcher.exec(readmeContent)[1], 10); + 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 newNIcons = overNIconsInReadme + updateRange; + + if (nIcons > newNIcons) { + const newContent = readmeContent.replace( + regexMatcher, + `Over ${newNIcons} `, + ); + await fs.writeFile(readmeFile, newContent); + } + } } catch (error) { console.error( - 'Failed to obtain number of SVG icons of current milestone in README:', + 'Failed to update number of SVG icons of current milestone in README:', error, ); process.exit(1); } - -const iconsData = await getIconsData(); -const nIcons = iconsData.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 474a4ba4a..ca5680166 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,12 +1,24 @@ +/** + * @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 {getDirnameFromImportMeta, getIconDataPath} from '../sdk.mjs'; const __dirname = getDirnameFromImportMeta(import.meta.url); +/** + * @typedef {import("../sdk.js").IconData} IconData + */ + /** * Get JSON schema data. - * @param {String} rootDirectory Path to the root directory of the project. + * @param {string} rootDirectory Path to the root directory of the project. + * @returns {Promise} JSON schema data. */ export const getJsonSchemaData = async ( rootDirectory = path.resolve(__dirname, '..'), @@ -18,8 +30,8 @@ export const getJsonSchemaData = async ( /** * Write icons data to _data/simple-icons.json. - * @param {Object} iconsData Icons data object. - * @param {String} rootDirectory Path to the root directory of the project. + * @param {{icons: IconData[]}} iconsData Icons data object. + * @param {string} rootDirectory Path to the root directory of the project. */ export const writeIconsData = async ( iconsData, diff --git a/sdk.d.ts b/sdk.d.ts index 184fe2283..4ad8726f4 100644 --- a/sdk.d.ts +++ b/sdk.d.ts @@ -1,5 +1,5 @@ /** - * @fileoverview + * @file * Types for Simple Icons SDK. */ @@ -10,7 +10,6 @@ import type {CustomLicense, SPDXLicense} from './types'; * * Includes the module and author of the extension, * both including a name and URL. - * * @see {@link https://github.com/simple-icons/simple-icons#third-party-extensions Third-Party Extensions} */ export type ThirdPartyExtension = { @@ -27,7 +26,6 @@ type ThirdPartyExtensionSubject = { * The aliases for a Simple Icon. * * Corresponds to the `aliases` property in the *_data/simple-icons.json* file. - * * @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#aliases Aliases} */ export type Aliases = { @@ -47,7 +45,6 @@ type DuplicateAlias = { * The data for a Simple Icon. * * Corresponds to the data stored for each icon in the *_data/simple-icons.json* file. - * * @see {@link https://github.com/mondeja/simple-icons/blob/utils-entrypoint/CONTRIBUTING.md#7-update-the-json-data-for-simpleiconsorg Update the JSON Data for SimpleIcons.org} */ export type IconData = { @@ -73,8 +70,8 @@ export function slugToVariableName(slug: string): string; export function titleToHtmlFriendly(brandTitle: string): string; export function htmlFriendlyToTitle(htmlFriendlyTitle: string): string; export function getIconDataPath(rootDirectory?: string): string; -export function getIconsDataString(rootDirectory?: string): string; -export function getIconsData(rootDirectory?: string): IconData[]; +export function getIconsDataString(rootDirectory?: string): Promise; +export function getIconsData(rootDirectory?: string): Promise; export function normalizeNewlines(text: string): string; export function normalizeColor(text: string): string; export function getThirdPartyExtensions( diff --git a/sdk.mjs b/sdk.mjs index 0fa791075..426773aff 100644 --- a/sdk.mjs +++ b/sdk.mjs @@ -1,5 +1,5 @@ /** - * @fileoverview + * @file * Simple Icons SDK. */ @@ -8,10 +8,11 @@ import path from 'node:path'; import {fileURLToPath} from 'node:url'; /** - * @typedef {import("./sdk").ThirdPartyExtension} ThirdPartyExtension - * @typedef {import("./sdk").IconData} IconData + * @typedef {import("./sdk.d.ts").ThirdPartyExtension} ThirdPartyExtension + * @typedef {import("./sdk.d.ts").IconData} IconData */ +/** @type {{ [key: string]: string }} */ const TITLE_TO_SLUG_REPLACEMENTS = { '+': 'plus', '.': 'dot', @@ -41,15 +42,15 @@ export const SVG_PATH_REGEX = /^m[-mzlhvcsqtae\d,. ]+$/i; /** * Get the directory name where this file is located from `import.meta.url`, * equivalent to the `__dirname` global variable in CommonJS. - * @param {String} importMetaUrl import.meta.url - * @returns {String} Directory name in which this file is located + * @param {string} importMetaUrl import.meta.url + * @returns {string} Directory name in which this file is located */ export const getDirnameFromImportMeta = (importMetaUrl) => path.dirname(fileURLToPath(importMetaUrl)); /** * Build a regex to validate HTTPs URLs. - * @param {String} jsonschemaPath Path to the *.jsonschema.json* file + * @param {string} jsonschemaPath Path to the *.jsonschema.json* file * @returns {Promise} Regex to validate HTTPs URLs */ export const urlRegex = async ( @@ -68,21 +69,21 @@ export const urlRegex = async ( /** * Get the slug/filename for an icon. * @param {IconData} icon The icon data as it appears in *_data/simple-icons.json* - * @returns {String} The slug/filename for the icon + * @returns {string} The slug/filename for the icon */ export const getIconSlug = (icon) => icon.slug || titleToSlug(icon.title); /** * Extract the path from an icon SVG content. - * @param {String} svg The icon SVG content - * @returns {String} The path from the icon SVG content - **/ + * @param {string} svg The icon SVG content + * @returns {string} The path from the icon SVG content + */ export const svgToPath = (svg) => svg.split('"', 8)[7]; /** * Converts a brand title into a slug/filename. - * @param {String} title The title to convert - * @returns {String} The slug/filename for the title + * @param {string} title The title to convert + * @returns {string} The slug/filename for the title */ export const titleToSlug = (title) => title @@ -96,8 +97,8 @@ export const titleToSlug = (title) => /** * Converts a slug into a variable name that can be exported. - * @param {String} slug The slug to convert - * @returns {String} The variable name for the slug + * @param {string} slug The slug to convert + * @returns {string} The variable name for the slug */ export const slugToVariableName = (slug) => { const slugFirstLetter = slug[0].toUpperCase(); @@ -107,8 +108,8 @@ export const slugToVariableName = (slug) => { /** * Converts a brand title as defined in *_data/simple-icons.json* into a brand * title in HTML/SVG friendly format. - * @param {String} brandTitle The title to convert - * @returns {String} The brand title in HTML/SVG friendly format + * @param {string} brandTitle The title to convert + * @returns {string} The brand title in HTML/SVG friendly format */ export const titleToHtmlFriendly = (brandTitle) => brandTitle @@ -117,6 +118,8 @@ export const titleToHtmlFriendly = (brandTitle) => .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll(/./g, (char) => { + /** @type {number} */ + // @ts-ignore const charCode = char.codePointAt(0); return charCode > 127 ? `&#${charCode};` : char; }); @@ -124,8 +127,8 @@ export const titleToHtmlFriendly = (brandTitle) => /** * Converts a brand title in HTML/SVG friendly format into a brand title (as * it is seen in *_data/simple-icons.json*) - * @param {String} htmlFriendlyTitle The title to convert - * @returns {String} The brand title in HTML/SVG friendly format + * @param {string} htmlFriendlyTitle The title to convert + * @returns {string} The brand title in HTML/SVG friendly format */ export const htmlFriendlyToTitle = (htmlFriendlyTitle) => htmlFriendlyTitle @@ -134,13 +137,18 @@ export const htmlFriendlyToTitle = (htmlFriendlyTitle) => ) .replaceAll( /&(quot|amp|lt|gt);/g, + /** + * @param {string} _ Full match + * @param {'quot' | 'amp' | 'lt' | 'gt'} reference Reference to replace + * @returns {string} Replacement for the reference + */ (_, reference) => ({quot: '"', amp: '&', lt: '<', gt: '>'})[reference], ); /** * Get path of *_data/simple-icons.json*. - * @param {String} rootDirectory Path to the root directory of the project - * @returns {String} Path of *_data/simple-icons.json* + * @param {string} rootDirectory Path to the root directory of the project + * @returns {string} Path of *_data/simple-icons.json* */ export const getIconDataPath = ( rootDirectory = getDirnameFromImportMeta(import.meta.url), @@ -150,8 +158,8 @@ export const getIconDataPath = ( /** * Get contents of *_data/simple-icons.json*. - * @param {String} rootDirectory Path to the root directory of the project - * @returns {String} Content of *_data/simple-icons.json* + * @param {string} rootDirectory Path to the root directory of the project + * @returns {Promise} Content of *_data/simple-icons.json* */ export const getIconsDataString = ( rootDirectory = getDirnameFromImportMeta(import.meta.url), @@ -161,8 +169,8 @@ export const getIconsDataString = ( /** * Get icons data as object from *_data/simple-icons.json*. - * @param {String} rootDirectory Path to the root directory of the project - * @returns {IconData[]} Icons data as array from *_data/simple-icons.json* + * @param {string} rootDirectory Path to the root directory of the project + * @returns {Promise} Icons data as array from *_data/simple-icons.json* */ export const getIconsData = async ( rootDirectory = getDirnameFromImportMeta(import.meta.url), @@ -173,8 +181,8 @@ export const getIconsData = async ( /** * Replace Windows newline characters by Unix ones. - * @param {String} text The text to replace - * @returns {String} The text with Windows newline characters replaced by Unix ones + * @param {string} text The text to replace + * @returns {string} The text with Windows newline characters replaced by Unix ones */ export const normalizeNewlines = (text) => { return text.replaceAll('\r\n', '\n'); @@ -182,8 +190,8 @@ export const normalizeNewlines = (text) => { /** * Convert non-6-digit hex color to 6-digit with the character `#` stripped. - * @param {String} text The color text - * @returns {String} The color text in 6-digit hex format + * @param {string} text The color text + * @returns {string} The color text in 6-digit hex format */ export const normalizeColor = (text) => { let color = text.replace('#', '').toUpperCase(); @@ -199,7 +207,7 @@ export const normalizeColor = (text) => { /** * Get information about third party extensions from the README table. - * @param {String} readmePath Path to the README file + * @param {string} readmePath Path to the README file * @returns {Promise} Information about third party extensions */ export const getThirdPartyExtensions = async ( @@ -214,23 +222,43 @@ export const getThirdPartyExtensions = async ( .split('|\n|') .slice(2) .map((line) => { - let [module, author] = line.split(' | '); - module = module.split('} Information about third party libraries */ export const getThirdPartyLibraries = async ( @@ -247,23 +275,42 @@ export const getThirdPartyLibraries = async ( .map((line) => { let [module, author] = line.split(' | '); module = module.split(' { /** * Get the index at which the text of the first `` tag starts. - * @param svgFileContent The raw SVG as text. - **/ + * @param {string} svgFileContent The raw SVG as text. + * @returns {number} The index at which the title text starts. + */ const getTitleTextIndex = (svgFileContent) => { const titleStart = ''; return svgFileContent.indexOf(titleStart) + titleStart.length; @@ -112,8 +140,9 @@ const getTitleTextIndex = (svgFileContent) => { /** * Convert a hexadecimal number passed as string to decimal number as integer. - * @param hex The hexadecimal number representation to convert. - **/ + * @param {string} hex The hexadecimal number representation to convert. + * @returns {number} The decimal number representation. + */ const hexadecimalToDecimal = (hex) => { let result = 0; let digitValue; @@ -125,6 +154,11 @@ const hexadecimalToDecimal = (hex) => { return result; }; +/** + * Shorten a string with ellipsis if it exceeds 20 characters. + * @param {string} string_ The string to shorten. + * @returns {string} The shortened string. + */ const maybeShortenedWithEllipsis = (string_) => { return string_.length > 20 ? `${string_.slice(0, 20)}...` : string_; }; @@ -132,21 +166,32 @@ const maybeShortenedWithEllipsis = (string_) => { /** * Memoize a function which accepts a single argument. * A second argument can be passed to be used as key. + * @param {(arg0: any) => any} function_ The function to memoize. + * @returns {(arg0: any) => any} The memoized function. */ const memoize = (function_) => { + /** @type {{ [key: string]: any }} */ const results = {}; - return (argument, defaultKey = null) => { - const key = defaultKey || argument; + /** + * Memoized function. + * @param {any} argument The argument to memoize. + * @returns {any} The result of the memoized function. + */ + return (argument) => { + results[argument] ||= function_(argument); - results[key] ||= function_(argument); - - return results[key]; + return results[argument]; }; }; -const getIconPath = memoize(($icon, _filepath) => $icon.find('path').attr('d')); +/** @typedef {import('cheerio').Cheerio<import('domhandler').Document>} Cheerio */ + +/** @type {($icon: Cheerio) => string} */ +const getIconPath = memoize(($icon) => $icon.find('path').attr('d')); +/** @type {(iconPath: string) => import('svg-path-segments').Segment[]} */ const getIconPathSegments = memoize((iconPath) => parsePath(iconPath)); +/** @type {(iconPath: string) => import('svg-path-bbox').BBox} */ const getIconPathBbox = memoize((iconPath) => svgPathBbox(iconPath)); if (updateIgnoreFile) { @@ -165,21 +210,34 @@ if (updateIgnoreFile) { }); } -const isIgnored = (linterName, path) => { +/** + * Check if an icon is ignored by a linter rule. + * @param {string} linterRule The name of the linter rule. + * @param {string} path SVG path of the icon. + * @returns {boolean} Whether the icon is ignored by the linter rule + */ +const isIgnored = (linterRule, path) => { return ( - iconIgnored[linterName] && Object.hasOwn(iconIgnored[linterName], path) + iconIgnored[linterRule] && Object.hasOwn(iconIgnored[linterRule], path) ); }; -const ignoreIcon = (linterName, path, $) => { - iconIgnored[linterName] ||= {}; +/** + * Ignore an icon for a linter rule. + * @param {string} linterRule The name of the linter rule. + * @param {string} path SVG path of the icon. + * @param {Cheerio} $ The SVG object + */ +const ignoreIcon = (linterRule, path, $) => { + iconIgnored[linterRule] ||= {}; const title = $.find('title').text(); const iconName = htmlFriendlyToTitle(title); - iconIgnored[linterName][path] = iconName; + iconIgnored[linterRule][path] = iconName; }; +/** @type {import('svglint').Config} */ const config = { rules: { elm: { @@ -213,6 +271,7 @@ const config = { }, ], custom: [ + // eslint-disable-next-line complexity (reporter, $, ast) => { reporter.name = 'icon-title'; @@ -307,6 +366,8 @@ const config = { encodedBuf.unshift(iconTitleText[i]); } else { // Encode all non ascii characters plus "'&<> (XML named entities) + /** @type {number} */ + // @ts-ignore Coerce to number const charDecimalCode = iconTitleText.codePointAt(i); if (charDecimalCode > 127) { @@ -337,8 +398,12 @@ const config = { // Check if there are some other encoded characters in decimal notation // which shouldn't be encoded - // eslint-disable-next-line unicorn/prefer-number-properties - for (const match of encodingMatches.filter((m) => !isNaN(m[2]))) { + for (const match of encodingMatches.filter((m) => { + // TODO: this fails using `Number.isNaN`, investigate + // @ts-ignore + // eslint-disable-next-line unicorn/prefer-number-properties + return !isNaN(m[2]); + })) { const decimalNumber = Number.parseInt(match[2], 10); if (decimalNumber > 127) { continue; @@ -378,10 +443,10 @@ const config = { } } }, - (reporter, $, ast, {filepath}) => { + (reporter, $) => { reporter.name = 'icon-size'; - const iconPath = getIconPath($, filepath); + const iconPath = getIconPath($); if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { return; } @@ -407,16 +472,19 @@ const config = { } } }, - (reporter, $, ast, {filepath}) => { + (reporter, $, ast) => { reporter.name = 'icon-precision'; - const iconPath = getIconPath($, filepath); + const iconPath = getIconPath($); const segments = getIconPathSegments(iconPath); for (const segment of segments) { + /** @type {number[]} */ + // @ts-ignore + const numberParameters = segment.params.slice(1); const precisionMax = Math.max( // eslint-disable-next-line unicorn/no-array-callback-reference - ...segment.params.slice(1).map(countDecimals), + ...numberParameters.map(countDecimals), ); if (precisionMax > iconMaxFloatPrecision) { let errorMessage = @@ -439,11 +507,16 @@ const config = { } } }, - (reporter, $, ast, {filepath}) => { + (reporter, $, ast) => { reporter.name = 'ineffective-segments'; - const iconPath = getIconPath($, filepath); + const iconPath = getIconPath($); const segments = getIconPathSegments(iconPath); + + /** @type {import('svg-path-segments').Segment[]} */ + // TODO: svgpath does not includes the segment property on the interface, + // see https://github.com/fontello/svgpath/pull/67/files + // @ts-ignore const absSegments = svgpath(iconPath).abs().unshort().segments; const lowerMovementCommands = ['m', 'l']; @@ -476,11 +549,16 @@ const config = { ...curveCommands, ]); - const isInvalidSegment = ( - [command, x1Coord, y1Coord, ...rest], - index, - previousSegmentIsZ, - ) => { + /** + * Check if a segment is ineffective. + * @param {import('svg-path-segments').Segment} segment The segment to check. + * @param {number} index The index of the segment in the path. + * @param {boolean} previousSegmentIsZ Whether the previous segment is a Z command. + * @returns {boolean} Whether the segment is ineffective. + */ + // eslint-disable-next-line complexity + const isInvalidSegment = (segment, index, previousSegmentIsZ) => { + const [command, x1Coord, y1Coord, ...rest] = segment.params; if (commands.has(command)) { // Relative directions (h or v) having a length of 0 if (lowerDirectionCommands.includes(command) && x1Coord === 0) { @@ -534,6 +612,7 @@ const config = { let [yPreviousCoordDeep, xPreviousCoordDeep] = [ ...absSegments[index_], ].reverse(); + // If the previous command was a horizontal movement, // we need to consider the single coordinate as x if (upperHorDirectionCommand === xPreviousCoordDeep) { @@ -609,6 +688,8 @@ const config = { ); } } + + return false; }; for (let index = 0; index < segments.length; index++) { @@ -616,7 +697,7 @@ const config = { const previousSegmentIsZ = index > 0 && segments[index - 1].params[0].toLowerCase() === 'z'; - if (isInvalidSegment(segment.params, index, previousSegmentIsZ)) { + if (isInvalidSegment(segment, index, previousSegmentIsZ)) { const [command, _x1, _y1, ...rest] = segment.params; let errorMessage = `Ineffective segment "${iconPath.slice( @@ -671,13 +752,15 @@ const config = { } } }, - (reporter, $, ast, {filepath}) => { + (reporter, $, ast) => { reporter.name = 'collinear-segments'; - /** * Extracts collinear coordinates from SVG path straight lines - * (does not extracts collinear coordinates from curves). - **/ + * (does not extracts collinear coordinates from curves). + * @param {string} iconPath The SVG path of the icon. + * @returns {import('svg-path-segments').Segment[]} The collinear segments. + */ + // eslint-disable-next-line complexity const getCollinearSegments = (iconPath) => { const segments = getIconPathSegments(iconPath); const collinearSegments = []; @@ -694,13 +777,18 @@ const config = { const seg = segments[s]; const parms = seg.params; const cmd = parms[0]; - const nextCmd = s + 1 < segments.length ? segments[s + 1][0] : null; + const nextCmd = + s + 1 < segments.length ? segments[s + 1].params[0] : null; switch (cmd) { // Next switch cases have been ordered by frequency // of occurrence in the SVG paths of the icons case 'M': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = parms[1]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = parms[2]; // SVG 1.1: // If a moveto is followed by multiple pairs of coordinates, @@ -713,7 +801,11 @@ const config = { } case 'm': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; if (seg.chain === undefined || seg.chain.start === seg.start) { startPoint = undefined; @@ -723,33 +815,49 @@ const config = { } case 'H': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = parms[1]; break; } case 'h': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; break; } case 'V': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = parms[1]; break; } case 'v': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[1]; break; } case 'L': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = parms[1]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = parms[2]; break; } case 'l': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; break; } @@ -763,61 +871,101 @@ const config = { } case 'C': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = parms[5]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = parms[6]; break; } case 'c': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[5]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[6]; break; } case 'A': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = parms[6]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = parms[7]; break; } case 'a': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[6]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[7]; break; } case 's': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; break; } case 'S': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = parms[1]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = parms[2]; break; } case 't': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[1]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[2]; break; } case 'T': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = parms[1]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = parms[2]; break; } case 'Q': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = parms[3]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = parms[4]; break; } case 'q': { + /** @type {number} */ + // @ts-ignore currentAbsCoord[0] = (currentAbsCoord[0] || 0) + parms[3]; + /** @type {number} */ + // @ts-ignore currentAbsCoord[1] = (currentAbsCoord[1] || 0) + parms[4]; break; } @@ -872,7 +1020,7 @@ const config = { return collinearSegments; }; - const iconPath = getIconPath($, filepath); + const iconPath = getIconPath($); const collinearSegments = getCollinearSegments(iconPath); if (collinearSegments.length === 0) { return; @@ -913,10 +1061,10 @@ const config = { } } }, - (reporter, $, ast, {filepath}) => { + (reporter, $, ast) => { reporter.name = 'negative-zeros'; - const iconPath = getIconPath($, filepath); + const iconPath = getIconPath($); // Find negative zeros inside path const negativeZeroMatches = [...iconPath.matchAll(negativeZerosRegexp)]; @@ -937,10 +1085,10 @@ const config = { } } }, - (reporter, $, ast, {filepath}) => { + (reporter, $) => { reporter.name = 'icon-centered'; - const iconPath = getIconPath($, filepath); + const iconPath = getIconPath($); if (!updateIgnoreFile && isIgnored(reporter.name, iconPath)) { return; } @@ -964,15 +1112,17 @@ const config = { } } }, - (reporter, $, ast, {filepath}) => { + (reporter, $, ast) => { reporter.name = 'final-closepath'; - const iconPath = getIconPath($, filepath); + const iconPath = getIconPath($); const segments = getIconPathSegments(iconPath); // Unnecessary characters after the final closepath + /** @type {import('svg-path-segments').Segment} */ + // @ts-ignore const lastSegment = segments.at(-1); - const endsWithZ = ['z', 'Z'].includes(lastSegment.params.at(0)); + const endsWithZ = ['z', 'Z'].includes(lastSegment.params[0]); if (endsWithZ && lastSegment.end - lastSegment.start > 1) { const ending = iconPath.slice(lastSegment.start + 1); const closepath = iconPath.at(lastSegment.start); @@ -985,10 +1135,10 @@ const config = { reporter.error(errorMessage); } }, - (reporter, $, ast, {filepath}) => { + (reporter, $, ast) => { reporter.name = 'path-format'; - const iconPath = getIconPath($, filepath); + const iconPath = getIconPath($); if (!SVG_PATH_REGEX.test(iconPath)) { const errorMessage = 'Invalid path format'; diff --git a/svgo.config.mjs b/svgo.config.mjs index f3e99960e..60ec75968 100644 --- a/svgo.config.mjs +++ b/svgo.config.mjs @@ -1,6 +1,10 @@ +/** + * @file SVGO configuration for Simple Icons. + */ + +/** @type {import("svgo").Config} */ const config = { multipass: true, - eol: 'lf', plugins: [ 'cleanupAttrs', 'inlineStyles', @@ -72,7 +76,6 @@ const config = { name: 'sortAttrs', params: { order: ['role', 'viewBox', 'xmlns'], - xmlnsOrder: 'end', }, }, 'sortDefsChildren', @@ -87,7 +90,6 @@ const config = { ], }, }, - 'removeElementsByAttr', { // Keep the role="img" attribute and automatically add it // to the <svg> tag if it's not there already diff --git a/tests/docs.test.js b/tests/docs.test.js index 9c5afeca6..ab4fa3735 100644 --- a/tests/docs.test.js +++ b/tests/docs.test.js @@ -1,3 +1,7 @@ +/** + * @file Tests for the documentation. + */ + import {strict as assert} from 'node:assert'; import {test} from 'mocha'; import {getThirdPartyExtensions, getThirdPartyLibraries} from '../sdk.mjs'; diff --git a/tests/index.test.js b/tests/index.test.js index 2b10b96f8..ad3552fa8 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -1,3 +1,9 @@ +/** + * @file Tests for the index file of npm package. + */ + +// The index.mjs file is generated on build before running tests +// @ts-ignore import * as simpleIcons from '../index.mjs'; import {getIconSlug, getIconsData, slugToVariableName} from '../sdk.mjs'; import {testIcon} from './test-icon.js'; @@ -5,6 +11,8 @@ import {testIcon} from './test-icon.js'; for (const icon of await getIconsData()) { const slug = getIconSlug(icon); const variableName = slugToVariableName(slug); + /** @type {import('../types.d.ts').SimpleIcon} */ + // @ts-ignore const subject = simpleIcons[variableName]; testIcon(icon, subject, slug); diff --git a/tests/min-reporter.cjs b/tests/min-reporter.cjs index 11f841097..a54a57908 100644 --- a/tests/min-reporter.cjs +++ b/tests/min-reporter.cjs @@ -1,8 +1,18 @@ +/** + * @file Custom mocha reporter. + * + * Serves to clear the console after the test run is finished. + * See {@link https://github.com/mochajs/mocha/issues/2312} + */ + const {reporters, Runner} = require('mocha'); const {EVENT_RUN_END} = Runner.constants; class EvenMoreMin extends reporters.Base { + /** + * @param {import('mocha').Runner} runner Mocha test runner + */ constructor(runner) { super(runner); runner.once(EVENT_RUN_END, () => this.epilogue()); diff --git a/tests/test-icon.js b/tests/test-icon.js index 409e13812..c9fe8f04b 100644 --- a/tests/test-icon.js +++ b/tests/test-icon.js @@ -1,3 +1,7 @@ +/** + * @file Icon tester. + */ + import {strict as assert} from 'node:assert'; import fs from 'node:fs/promises'; import path from 'node:path'; @@ -14,15 +18,11 @@ const iconsDirectory = path.resolve( 'icons', ); -/** - * @typedef {import('..').SimpleIcon} SimpleIcon - */ - /** * Checks if icon data matches a subject icon. - * @param {SimpleIcon} icon Icon data - * @param {SimpleIcon} subject Icon to check against icon data - * @param {String} slug Icon data slug + * @param {import('../sdk.d.ts').IconData} icon Icon data + * @param {import('../types.d.ts').SimpleIcon} subject Icon object to check against icon data + * @param {string} slug Icon data slug */ export const testIcon = (icon, subject, slug) => { const svgPath = path.resolve(iconsDirectory, `${slug}.svg`); @@ -62,8 +62,10 @@ export const testIcon = (icon, subject, slug) => { it(`has ${icon.license ? 'the correct' : 'no'} "license"`, () => { if (icon.license) { - assert.equal(subject.license.type, icon.license.type); + assert.equal(subject.license?.type, icon.license.type); if (icon.license.type === 'custom') { + // TODO: `Omit` not working smoothly here + // @ts-ignore assert.equal(subject.license.url, icon.license.url); } } else { diff --git a/types.d.ts b/types.d.ts index ab2ea35ac..202425a11 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,6 +1,9 @@ +/** + * @file Types for Simple Icons package. + */ + /** * The license for a Simple Icon. - * * @see {@link https://github.com/simple-icons/simple-icons/blob/develop/CONTRIBUTING.md#optional-data Optional Data} */ export type License = SPDXLicense | CustomLicense;