mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
The downloader spoke to a remote instance through gitea.dev/sdk, the last external dependency this tree could not name for itself. We migrate from GitHub, so the path earned nothing and is gone, along with its service enum name, migrate form, locale string, mock fixtures and icon rules. Enum slot 3 stays reserved: repositories already record it in original_service_type, and renumbering would change what those rows mean. TestDumpRestore went with it. It dumped from our own instance through that downloader, so the round trip has no coverage until a downloader that speaks our own API exists. Co-authored-by: Hanzo Dev <[email protected]>
146 lines
5.7 KiB
JavaScript
Executable File
146 lines
5.7 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
import {optimize} from 'svgo';
|
|
import {dirname, parse} from 'node:path';
|
|
import {globSync, writeFileSync} from 'node:fs';
|
|
import {readFile, writeFile, mkdir} from 'node:fs/promises';
|
|
import {fileURLToPath} from 'node:url';
|
|
import {exit} from 'node:process';
|
|
import type {Manifest} from 'material-icon-theme';
|
|
|
|
const glob = (pattern: string) => globSync(pattern, {cwd: dirname(import.meta.dirname)});
|
|
|
|
type Opts = {
|
|
prefix?: string,
|
|
fullName?: string,
|
|
};
|
|
|
|
async function processAssetsSvgFile(path: string, {prefix, fullName}: Opts = {}) {
|
|
let name = fullName;
|
|
if (!name) {
|
|
name = parse(path).name;
|
|
if (prefix) name = `${prefix}-${name}`;
|
|
if (prefix === 'octicon') name = name.replace(/-[0-9]+$/, ''); // chop of '-16' on octicons
|
|
}
|
|
// Set the `xmlns` attribute so that the files are displayable in standalone documents
|
|
// The svg backend module will strip the attribute during startup for inline display
|
|
const {data} = optimize(await readFile(path, 'utf8'), {
|
|
plugins: [
|
|
{name: 'preset-default'},
|
|
{name: 'removeDimensions'},
|
|
{name: 'removeTitle'},
|
|
{name: 'prefixIds', params: {prefix: () => name}},
|
|
{name: 'addClassesToSVGElement', params: {classNames: ['svg', name]}},
|
|
{
|
|
name: 'addAttributesToSVGElement', params: {
|
|
attributes: [
|
|
{'xmlns': 'http://www.w3.org/2000/svg'},
|
|
{'width': '16'}, {'height': '16'}, {'aria-hidden': 'true'},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
await writeFile(fileURLToPath(new URL(`../public/assets/img/svg/${name}.svg`, import.meta.url)), data);
|
|
}
|
|
|
|
function processAssetsSvgFiles(pattern: string, opts: Opts = {}) {
|
|
return glob(pattern).map((path) => processAssetsSvgFile(path, opts));
|
|
}
|
|
|
|
// Names carried by the upstream icon theme that this product has no rule for.
|
|
const unusedIcons = ['gitea'];
|
|
function unusedIconName(name: string) {
|
|
return unusedIcons.some((n) => name.toLowerCase().includes(n));
|
|
}
|
|
|
|
function lowercaseKeys(obj: Record<string, any>) {
|
|
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]));
|
|
}
|
|
|
|
async function processMaterialFileIcons() {
|
|
const paths = glob('node_modules/material-icon-theme/icons/*.svg');
|
|
const svgSymbols: Record<string, string> = {};
|
|
for (const path of paths) {
|
|
const svgName = parse(path).name;
|
|
if (unusedIconName(svgName)) continue;
|
|
// remove all unnecessary attributes, only keep "viewBox"
|
|
const {data} = optimize(await readFile(path, 'utf8'), {
|
|
plugins: [
|
|
{name: 'preset-default'},
|
|
{name: 'removeDimensions'},
|
|
{name: 'removeXMLNS'},
|
|
{name: 'removeAttrs', params: {attrs: 'xml:space', elemSeparator: ','}},
|
|
],
|
|
});
|
|
// intentionally use single quote here to avoid escaping
|
|
svgSymbols[svgName] = data.replace(/"/g, `'`);
|
|
}
|
|
writeFileSync(fileURLToPath(new URL(`../options/fileicon/material-icon-svgs.json`, import.meta.url)), JSON.stringify(svgSymbols, null, 2));
|
|
|
|
const vscodeExtensionsJson = await readFile(fileURLToPath(new URL(`generate-svg-vscode-extensions.json`, import.meta.url)), 'utf8');
|
|
const vscodeExtensions = JSON.parse(vscodeExtensionsJson) as Record<string, string>;
|
|
const iconRulesJson = await readFile(fileURLToPath(new URL(`../node_modules/material-icon-theme/dist/material-icons.json`, import.meta.url)), 'utf8');
|
|
const iconRules = JSON.parse(iconRulesJson) as Manifest;
|
|
// The rules are from VSCode material-icon-theme, we need to adjust them to our needs
|
|
// 1. We only use lowercase filenames to match (it should be good enough for most cases and more efficient)
|
|
// 2. We do not have a "Language ID" system:
|
|
// * https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers
|
|
// * https://github.com/microsoft/vscode/tree/1.98.0/extensions
|
|
delete iconRules.iconDefinitions;
|
|
|
|
if (iconRules.fileNames) {
|
|
iconRules.fileNames = lowercaseKeys(iconRules.fileNames);
|
|
}
|
|
if (iconRules.folderNames) {
|
|
iconRules.folderNames = lowercaseKeys(iconRules.folderNames);
|
|
}
|
|
if (iconRules.fileExtensions) {
|
|
iconRules.fileExtensions = lowercaseKeys(iconRules.fileExtensions);
|
|
}
|
|
|
|
// Use VSCode's "Language ID" mapping from its extensions
|
|
for (const langIdExtMap of Object.values(vscodeExtensions)) {
|
|
for (const [langId, names] of Object.entries(langIdExtMap)) {
|
|
for (const name of names) {
|
|
const nameLower = name.toLowerCase();
|
|
if (nameLower[0] === '.') {
|
|
if (iconRules.fileExtensions) {
|
|
iconRules.fileExtensions[nameLower.substring(1)] ??= langId;
|
|
}
|
|
} else {
|
|
if (iconRules.fileNames) {
|
|
iconRules.fileNames[nameLower] ??= langId;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Drop rules for names the theme carries but we have no use for.
|
|
for (const section of [iconRules.fileNames, iconRules.folderNames, iconRules.folderNamesExpanded, iconRules.fileExtensions]) {
|
|
if (!section) continue;
|
|
for (const key of Object.keys(section)) {
|
|
if (unusedIconName(key) || unusedIconName(section[key])) delete section[key];
|
|
}
|
|
}
|
|
|
|
const iconRulesPretty = JSON.stringify(iconRules, null, 2);
|
|
writeFileSync(fileURLToPath(new URL(`../options/fileicon/material-icon-rules.json`, import.meta.url)), iconRulesPretty);
|
|
}
|
|
|
|
async function main() {
|
|
await mkdir(fileURLToPath(new URL('../public/assets/img/svg', import.meta.url)), {recursive: true});
|
|
await Promise.all([
|
|
...processAssetsSvgFiles('node_modules/@primer/octicons/build/svg/*-16.svg', {prefix: 'octicon'}),
|
|
...processAssetsSvgFiles('web_src/svg/*.svg'),
|
|
...processAssetsSvgFiles('public/assets/img/logo.svg', {fullName: 'hanzo-hanzo'}),
|
|
processMaterialFileIcons(),
|
|
]);
|
|
}
|
|
|
|
try {
|
|
await main();
|
|
} catch (err) {
|
|
console.error(err);
|
|
exit(1);
|
|
}
|