1/* eslint-disable no-restricted-syntax, no-await-in-loop */
2import fs from 'fs';
3import path from 'path';
4import { simpleGit } from 'simple-git';
5import semver from 'semver';
6import { readSvgDirectory } from '@lucide/helpers';
7
8const DATE_OF_FORK = '2020-06-08T16:39:52+0100';
9
10const git = simpleGit();
11
12const currentDir = process.cwd();
13const ICONS_DIR = path.resolve(currentDir, '../icons');
14const iconJsonFiles = readSvgDirectory(ICONS_DIR, '.json');
15const location = path.resolve(currentDir, '.vitepress/data', 'releaseMetaData.json');
16const releaseMetaDataDirectory = path.resolve(currentDir, '.vitepress/data', 'releaseMetadata');
17
18const allowedIconNameWithDoubleRelease = ['slash'];
19
20if (fs.existsSync(location)) {
21 fs.unlinkSync(location);
22}
23
24if (fs.existsSync(releaseMetaDataDirectory)) {
25 fs.rmSync(releaseMetaDataDirectory, { recursive: true, force: true });
26}
27
28if (!fs.existsSync(releaseMetaDataDirectory)) {
29 fs.mkdirSync(releaseMetaDataDirectory);
30}
31
32const fetchAllReleases = async () => {
33 await git.fetch('https://github.com/lucide-icons/lucide.git', '--tags');
34
35 return Promise.all(
36 (await git.tag(['-l']))
37 .trim()
38 .split(/\n/)
39 .filter((tag) => semver.valid(tag))
40 .sort(semver.compare),
41 );
42};
43
44const tags = await fetchAllReleases();
45
46const comparisonsPromises = tags.map(async (tag, index) => {
47 const previousTag = tags[index - 1];
48
49 if (!previousTag) return undefined;
50
51 const diff = await git.diff(['--name-status', '--oneline', previousTag, tag]);
52 const files = diff.split('\n').map((line) => {
53 const [status, file, renamedFile] = line.split('\t');
54
55 return { status, file, renamedFile };
56 });
57
58 const iconFiles = files.filter(({ file }) => file != null && file.startsWith('icons/'));
59 let date = (await git.show(['-s', '--format=%cI', tag])).trim();
60
61 // Fallback to dat of fork if date is not valid
62 if (!date.startsWith('20')) {
63 date = DATE_OF_FORK;
64 }
65
66 return {
67 tag,
68 date,
69 iconFiles,
70 };
71});
72
73const comparisons = await Promise.all(comparisonsPromises);
74const newReleaseMetaData = {};
75
76comparisons.forEach(({ tag, iconFiles, date } = {}) => {
77 if (tag == null) return;
78
79 iconFiles.forEach(({ status, file, renamedFile }) => {
80 if (file.endsWith('.json')) return;
81
82 const version = tag.replace('v', '');
83 const iconName = path.basename(file, '.svg');
84
85 if (newReleaseMetaData[iconName] == null) newReleaseMetaData[iconName] = {};
86
87 const releaseData = {
88 version,
89 date,
90 };
91
92 if (status.startsWith('R')) {
93 // Make sure set the old one as well
94 newReleaseMetaData[iconName].changedRelease = {
95 version,
96 date,
97 };
98
99 const renamedIconName = path.basename(renamedFile, '.svg');
100
101 if (newReleaseMetaData[renamedIconName] == null) {
102 newReleaseMetaData[renamedIconName] = {};
103 }
104
105 newReleaseMetaData[renamedIconName].changedRelease = {
106 version,
107 date,
108 };
109 }
110
111 if (status === 'A') {
112 if (
113 'changedRelease' in newReleaseMetaData[iconName] &&
114 !allowedIconNameWithDoubleRelease.includes(iconName)
115 ) {
116 throw new Error(`Icon '${iconName}' has already changedRelease set.`);
117 }
118
119 newReleaseMetaData[iconName].createdRelease = releaseData;
120 newReleaseMetaData[iconName].changedRelease = releaseData;
121 }
122 if (status === 'M') {
123 newReleaseMetaData[iconName].changedRelease = {
124 version,
125 date,
126 };
127 }
128 });
129});
130
131const defaultReleaseMetaData = {
132 createdRelease: {
133 version: '0.0.0',
134 date: DATE_OF_FORK,
135 },
136 changedRelease: {
137 version: '0.0.0',
138 date: DATE_OF_FORK,
139 },
140};
141
142try {
143 const releaseMetaData = await Promise.all(
144 iconJsonFiles.map(async (iconJsonFile) => {
145 const iconName = path.basename(iconJsonFile, '.json');
146 const metaDir = path.resolve(releaseMetaDataDirectory, `${iconName}.json`);
147
148 if (!(iconName in newReleaseMetaData)) {
149 console.error(`Could not find release metadata for icon '${iconName}'.`);
150 }
151
152 const contents = {
153 ...defaultReleaseMetaData,
154 ...(newReleaseMetaData[iconName] ?? {}),
155 };
156
157 const metaData = await fs.promises.readFile(path.join(ICONS_DIR, iconJsonFile), 'utf-8');
158 const iconMetaData = JSON.parse(metaData);
159 const aliases = iconMetaData.aliases ?? [];
160
161 if (aliases.length) {
162 aliases
163 .map((alias) => (typeof alias === 'string' ? alias : alias.name))
164 .forEach((alias) => {
165 if (!(alias in newReleaseMetaData)) {
166 return;
167 }
168
169 contents.createdRelease =
170 newReleaseMetaData[alias].createdRelease ?? defaultReleaseMetaData.createdRelease;
171 });
172 }
173
174 const output = JSON.stringify(contents, null, 2);
175 await fs.promises.writeFile(metaDir, output, 'utf-8');
176
177 return [iconName, contents];
178 }),
179 );
180 await fs.promises.writeFile(
181 location,
182 JSON.stringify(Object.fromEntries(releaseMetaData), null, 2),
183 'utf-8',
184 );
185
186 console.log('Successfully written icon release meta files');
187} catch (error) {
188 throw new Error(`Something went wrong generating icon release meta cache file,\n ${error}`);
189}