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
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('');
|
|
});
|
|
}
|
|
|