A set of over 6000 free MIT-licensed high-quality SVG icons for you to use in your web projects.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

220 lines
6.9 KiB

// Prints the notes of the current release, published as a single GitHub
// release for all packages: the preview image of the new icons, the list of the
// new icons, the icons renamed since the previous release and the changes from
// the changelogs of every package for this version, in the format of the
// previous releases:
//
// ### 18 new icons:
//
// - `outline/gpu`
//
// Renamed icons:
//
// - `outline/foodsteps` renamed to `outline/footsteps`
//
// ### Fixes and improvements
//
// - **Webfont:** Fix the `home` glyph (#1616, thanks @contributor)
//
// An entry shared by several packages is listed once, "Updated dependencies"
// entries and the thanks to the maintainers are left out.
//
// --ref <git ref> ref the image is loaded from (default: the `v<version>` tag)
// --since <git ref> ref the renamed icons are compared to (default: the tag
// of the previous release)
// --image-only print just the image
import { execFileSync } from 'child_process';
import { existsSync, readFileSync } from 'fs';
import { globSync } from 'glob';
import { dirname, resolve } from 'path';
import { GITHUB_DIR, HOME_DIR, PACKAGES_DIR, getArgvs } from './helpers.mjs';
const MAIN_PACKAGE = '@tabler/icons';
const MAINTAINERS = ['codecalm'];
// Changelog sections of Changesets and their headings in the release notes
const CHANGE_TYPES = {
Major: 'Breaking changes',
Minor: 'New features',
Patch: 'Fixes and improvements',
};
const PACKAGE_LABELS = {
'@tabler/icons-angular': 'Angular',
'@tabler/icons-astro': 'Astro',
'@tabler/icons-eps': 'EPS',
'@tabler/icons-pdf': 'PDF',
'@tabler/icons-png': 'PNG',
'@tabler/icons-preact': 'Preact',
'@tabler/icons-react': 'React',
'@tabler/icons-react-native': 'React Native',
'@tabler/icons-solidjs': 'SolidJS',
'@tabler/icons-sprite': 'Sprite',
'@tabler/icons-svelte': 'Svelte',
'@tabler/icons-svelte-runes': 'Svelte Runes',
'@tabler/icons-vue': 'Vue',
'@tabler/icons-webfont': 'Webfont',
};
// Entry of the changeset generated for the new icons (see release-changeset.mjs)
const NEW_ICONS_REGEX = /^\d+ new icons?$/;
// "- [#1](url) [`sha`](url) Thanks [@user](url)! - Summary" generated by
// @changesets/changelog-github, every part before the summary is optional
const ENTRY_REGEX =
/^- (?:(?:\[#(\d+)\]\([^)]+\))? ?(?:\[`[0-9a-f]+`\]\([^)]+\))? ?(?:Thanks (.+?)!)? - )?(.*)$/;
const git = (...args) =>
execFileSync('git', args, {
cwd: HOME_DIR,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
// Icons renamed since the previous release as [from, to] pairs, read from git
// as renames need no changeset
const getRenamedIcons = (version, since) => {
try {
since ||= git(
'describe',
'--tags',
'--abbrev=0',
'--match',
'v[0-9]*',
'--exclude',
`v${version}`,
);
const diff = git('diff', since, 'HEAD', '--name-status', '--diff-filter=R', '--', 'icons');
return [...diff.matchAll(/^R\d+\s+icons\/(\S+)\.svg\s+icons\/(\S+)\.svg$/gm)].map(
([, from, to]) => [from, to],
);
} catch {
// no previous release or no git history (shallow clone)
return [];
}
};
// Changes the entry to "- Summary (#1, thanks @user)"
const formatEntry = (entry) => {
const [firstLine, ...continuation] = entry.split('\n');
const [, pull, users = '', summary] = firstLine.match(ENTRY_REGEX);
const contributors = [...users.matchAll(/\[@([^\]]+)\]/g)]
.map(([, user]) => user)
.filter((user) => !MAINTAINERS.includes(user));
const refs = [
pull && `#${pull}`,
contributors.length > 0 && `thanks ${contributors.map((user) => `@${user}`).join(', ')}`,
].filter(Boolean);
return [`- ${summary}${refs.length > 0 ? ` (${refs.join(', ')})` : ''}`, ...continuation].join(
'\n',
);
};
const { version } = JSON.parse(readFileSync(resolve(PACKAGES_DIR, 'icons/package.json'), 'utf-8'));
const argvs = getArgvs();
const repo = process.env.GITHUB_REPOSITORY || 'tabler/tabler-icons';
const ref = argvs.ref || `v${version}`;
const image = `tabler-icons-${version}@2x.png`;
if (existsSync(resolve(GITHUB_DIR, image))) {
console.log(
`<img width="584" src="https://raw.githubusercontent.com/${repo}/${ref}/.github/${image}" />\n`,
);
}
if (!argvs['image-only']) {
const escapedVersion = version.replace(/\./g, '\\.');
// { [type]: Map<entry, packages[]> } in the order the entries first appear,
// starting with the main package
const changes = Object.fromEntries(Object.keys(CHANGE_TYPES).map((type) => [type, new Map()]));
const packages = globSync(resolve(PACKAGES_DIR, '*/CHANGELOG.md'))
.map((changelogPath) => ({
name: JSON.parse(readFileSync(resolve(dirname(changelogPath), 'package.json'), 'utf-8')).name,
changelog: readFileSync(changelogPath, 'utf-8'),
}))
.sort((a, b) => (a.name === MAIN_PACKAGE ? -1 : b.name === MAIN_PACKAGE ? 1 : 0));
packages.forEach(({ name, changelog }) => {
const [, section = ''] =
changelog.match(
new RegExp(`^## ${escapedVersion}\\n([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, 'm'),
) || [];
Object.keys(CHANGE_TYPES).forEach((type) => {
const [, list = ''] =
section.match(
new RegExp(`^### ${type} Changes\\n([\\s\\S]*?)(?=^### |(?![\\s\\S]))`, 'm'),
) || [];
// Top-level list items, together with their indented continuation lines
list
.split(/^(?=- )/m)
.map((entry) => entry.trim())
.filter((entry) => entry.startsWith('- ') && !entry.startsWith('- Updated dependencies'))
.map(formatEntry)
.forEach((entry) => {
changes[type].set(entry, [...(changes[type].get(entry) || []), name]);
});
});
});
// The new icons go first, as a section of their own
Object.values(changes).forEach((entries) => {
entries.forEach((names, entry) => {
const [firstLine, ...icons] = entry.split('\n');
if (!NEW_ICONS_REGEX.test(firstLine.slice(2))) {
return;
}
console.log(`### ${firstLine.slice(2)}:\n`);
console.log(
icons
.map((line) => line.trim())
.filter(Boolean)
.join('\n'),
);
console.log('');
entries.delete(entry);
});
});
const renamedIcons = getRenamedIcons(version, argvs.since);
if (renamedIcons.length > 0) {
console.log('Renamed icons:\n');
renamedIcons.forEach(([from, to]) => console.log(`- \`${from}\` renamed to \`${to}\``));
console.log('');
}
Object.entries(CHANGE_TYPES).forEach(([type, heading]) => {
if (changes[type].size === 0) {
return;
}
console.log(`### ${heading}\n`);
changes[type].forEach((names, entry) => {
// Entries of the main package (icons) need no label
const label = names.includes(MAIN_PACKAGE)
? ''
: `**${names
.map((name) => PACKAGE_LABELS[name] || name)
.sort()
.join(', ')}:** `;
console.log(entry.replace(/^- /, `- ${label}`));
});
console.log('');
});
}