@mindbase/mindbase 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/bin/gitlog.ts +3 -0
- package/bin/iconfont-editor.ts +3 -0
- package/bin/index.ts +29 -0
- package/bin/nodeclear.ts +3 -0
- package/bin/npmpublish.ts +3 -0
- package/bin/shared.ts +141 -0
- package/package.json +63 -0
- package/scripts/ensure-tsx.cjs +18 -0
- package/src/clear/app.ts +152 -0
- package/src/clear/config.ts +108 -0
- package/src/clear/file-cleaner.ts +137 -0
- package/src/clear/index.ts +28 -0
- package/src/clear/scanner.ts +154 -0
- package/src/gitlog/app.ts +272 -0
- package/src/gitlog/config.ts +91 -0
- package/src/gitlog/display.ts +178 -0
- package/src/gitlog/index.ts +5 -0
- package/src/gitlog/log-fetcher.ts +150 -0
- package/src/gitlog/pager.ts +178 -0
- package/src/gitlog/scanner.ts +60 -0
- package/src/iconfont/frontend/index.html +12 -0
- package/src/iconfont/frontend/src/App.vue +14 -0
- package/src/iconfont/frontend/src/main.ts +8 -0
- package/src/iconfont/frontend/src/views/IconEditor.vue +819 -0
- package/src/iconfont/frontend/vite.config.ts +15 -0
- package/src/iconfont/lib/css-generator.js +37 -0
- package/src/iconfont/lib/font-builder.js +82 -0
- package/src/iconfont/lib/glyph-extractor.js +69 -0
- package/src/iconfont/server/api.js +256 -0
- package/src/iconfont/server/index.js +64 -0
- package/src/index.ts +0 -0
- package/src/publish/app.ts +316 -0
- package/src/publish/builder.ts +120 -0
- package/src/publish/dependency.ts +144 -0
- package/src/publish/detector.ts +93 -0
- package/src/publish/index.ts +66 -0
- package/src/publish/npm-query.ts +244 -0
- package/src/publish/registry/adapters/npm.ts +128 -0
- package/src/publish/registry/registry-manager.ts +107 -0
- package/src/publish/scanner.ts +90 -0
- package/src/publish/types.ts +98 -0
- package/src/publish/version.ts +108 -0
- package/src/shared/config-manager.ts +57 -0
- package/src/shared/index.ts +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import vue from "@vitejs/plugin-vue";
|
|
3
|
+
import { resolve, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [vue()],
|
|
10
|
+
root: __dirname,
|
|
11
|
+
build: {
|
|
12
|
+
outDir: resolve(__dirname, "../dist"),
|
|
13
|
+
emptyOutDir: true,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS 生成模块
|
|
3
|
+
* 根据 iconfont.json 生成对应的 CSS 文件
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 生成 iconfont.css 内容
|
|
8
|
+
* @param {object} json - iconfont.json 数据
|
|
9
|
+
* @returns {string} CSS 内容
|
|
10
|
+
*/
|
|
11
|
+
export function buildCSS(json) {
|
|
12
|
+
const t = Date.now();
|
|
13
|
+
const fontFamily = json.font_family || "iconfont";
|
|
14
|
+
const prefix = json.css_prefix_text || "icon-";
|
|
15
|
+
|
|
16
|
+
let css = `@font-face {\n`;
|
|
17
|
+
css += ` font-family: "${fontFamily}";\n`;
|
|
18
|
+
css += ` src: url('iconfont.woff2?t=${t}') format('woff2'),\n`;
|
|
19
|
+
css += ` url('iconfont.woff?t=${t}') format('woff'),\n`;
|
|
20
|
+
css += ` url('iconfont.ttf?t=${t}') format('truetype');\n`;
|
|
21
|
+
css += `}\n\n`;
|
|
22
|
+
css += `.${fontFamily} {\n`;
|
|
23
|
+
css += ` font-family: "${fontFamily}" !important;\n`;
|
|
24
|
+
css += ` font-size: 16px;\n`;
|
|
25
|
+
css += ` font-style: normal;\n`;
|
|
26
|
+
css += ` -webkit-font-smoothing: antialiased;\n`;
|
|
27
|
+
css += ` -moz-osx-font-smoothing: grayscale;\n`;
|
|
28
|
+
css += `}\n\n`;
|
|
29
|
+
|
|
30
|
+
for (const g of json.glyphs) {
|
|
31
|
+
css += `.${prefix}${g.font_class}:before {\n`;
|
|
32
|
+
css += ` content: "\\${g.unicode}";\n`;
|
|
33
|
+
css += `}\n\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return css;
|
|
37
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字体构建模块
|
|
3
|
+
* 从 SVG 文件集合构建完整的 iconfont 字体(TTF/WOFF/WOFF2)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Readable } from "stream";
|
|
7
|
+
import svgicons2svgfont from "svgicons2svgfont";
|
|
8
|
+
import svg2ttf from "svg2ttf";
|
|
9
|
+
import subsetFont from "subset-font";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 将 SVG 集合构建为 SVG font,再转为 TTF Buffer
|
|
13
|
+
* @param {Array<{unicode: string, svg: string, name: string}>} icons - 图标集合
|
|
14
|
+
* @param {object} options - 配置
|
|
15
|
+
* @param {string} options.fontFamily - 字体族名
|
|
16
|
+
* @param {string} options.cssPrefix - CSS 前缀
|
|
17
|
+
* @returns {Promise<Buffer>} TTF Buffer
|
|
18
|
+
*/
|
|
19
|
+
export async function buildTtfFromIcons(icons, options) {
|
|
20
|
+
const fontFamily = options.fontFamily || "iconfont";
|
|
21
|
+
|
|
22
|
+
// 1. SVG → SVG font
|
|
23
|
+
const svgFont = await buildSvgFont(icons, fontFamily);
|
|
24
|
+
|
|
25
|
+
// 2. SVG font → TTF
|
|
26
|
+
const ttf = svg2ttf(svgFont, {});
|
|
27
|
+
return Buffer.from(ttf.buffer);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 从 TTF Buffer 生成 WOFF 和 WOFF2
|
|
32
|
+
* @param {Buffer} ttfBuffer - TTF Buffer
|
|
33
|
+
* @param {Array} glyphs - glyphs 数组(用于构建保留字符)
|
|
34
|
+
* @returns {Promise<{woff: Buffer, woff2: Buffer}>}
|
|
35
|
+
*/
|
|
36
|
+
export async function buildWebFonts(ttfBuffer, glyphs) {
|
|
37
|
+
// 构建保留字符文本
|
|
38
|
+
const keepChars = glyphs
|
|
39
|
+
.map((g) => String.fromCodePoint(parseInt(g.unicode, 16)))
|
|
40
|
+
.join("");
|
|
41
|
+
|
|
42
|
+
const [woff, woff2] = await Promise.all([
|
|
43
|
+
subsetFont(ttfBuffer, keepChars, { targetFormat: "woff" }),
|
|
44
|
+
subsetFont(ttfBuffer, keepChars, { targetFormat: "woff2" }),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
woff: Buffer.from(woff),
|
|
49
|
+
woff2: Buffer.from(woff2),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 用 svgicons2svgfont 将 SVG 集合转为 SVG font
|
|
55
|
+
*/
|
|
56
|
+
function buildSvgFont(icons, fontFamily) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
let output = "";
|
|
59
|
+
|
|
60
|
+
const stream = svgicons2svgfont({
|
|
61
|
+
fontName: fontFamily,
|
|
62
|
+
fontHeight: 1000,
|
|
63
|
+
normalize: true,
|
|
64
|
+
log: () => {},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
stream.on("data", (chunk) => (output += chunk));
|
|
68
|
+
stream.on("end", () => resolve(output));
|
|
69
|
+
stream.on("error", reject);
|
|
70
|
+
|
|
71
|
+
for (const icon of icons) {
|
|
72
|
+
const glyph = Readable.from([icon.svg]);
|
|
73
|
+
glyph.metadata = {
|
|
74
|
+
unicode: [String.fromCodePoint(parseInt(icon.unicode, 16))],
|
|
75
|
+
name: icon.name || icon.unicode,
|
|
76
|
+
};
|
|
77
|
+
stream.write(glyph);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
stream.end();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字形提取模块
|
|
3
|
+
* 从 TTF 字体中提取单个字形路径,转为 SVG
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import opentype from "opentype.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 从 TTF Buffer 中提取指定 unicode 的字形,返回 SVG 字符串
|
|
10
|
+
* @param {Buffer} ttfBuffer - TTF 字体文件 Buffer
|
|
11
|
+
* @param {string} unicode - 十六进制 unicode(如 "e9e3")
|
|
12
|
+
* @returns {string} SVG 内容
|
|
13
|
+
*/
|
|
14
|
+
export function extractGlyphToSvg(ttfBuffer, unicode) {
|
|
15
|
+
const font = opentype.parse(ttfBuffer.buffer);
|
|
16
|
+
const codePoint = parseInt(unicode, 16);
|
|
17
|
+
const glyph = font.glyphs.glyphs.find(
|
|
18
|
+
(g) => g.unicode === codePoint
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (!glyph) {
|
|
22
|
+
throw new Error(`未找到 unicode: ${unicode} 的字形`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 获取字形路径,使用 font 的 unitsPerEm 作为 viewBox
|
|
26
|
+
const unitsPerEm = font.unitsPerEm;
|
|
27
|
+
const ascender = font.ascender;
|
|
28
|
+
const path = glyph.getPath(0, ascender, unitsPerEm);
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${unitsPerEm} ${unitsPerEm}">`,
|
|
32
|
+
` <path d="${path.toPathData()}"/>`,
|
|
33
|
+
`</svg>`,
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 从 TTF Buffer 中提取所有字形,每个转为 SVG 字符串
|
|
39
|
+
* @param {Buffer} ttfBuffer - TTF 字体文件 Buffer
|
|
40
|
+
* @param {Array} glyphs - iconfont.json 中的 glyphs 数组
|
|
41
|
+
* @returns {Map<string, string>} unicode → SVG 内容的映射
|
|
42
|
+
*/
|
|
43
|
+
export function extractAllGlyphsToSvg(ttfBuffer, glyphs) {
|
|
44
|
+
const font = opentype.parse(ttfBuffer.buffer);
|
|
45
|
+
const result = new Map();
|
|
46
|
+
|
|
47
|
+
for (const g of glyphs) {
|
|
48
|
+
const codePoint = parseInt(g.unicode, 16);
|
|
49
|
+
const glyph = font.glyphs.glyphs.find(
|
|
50
|
+
(ft) => ft.unicode === codePoint
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (!glyph) continue;
|
|
54
|
+
|
|
55
|
+
const unitsPerEm = font.unitsPerEm;
|
|
56
|
+
const ascender = font.ascender;
|
|
57
|
+
const path = glyph.getPath(0, ascender, unitsPerEm);
|
|
58
|
+
|
|
59
|
+
const svg = [
|
|
60
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${unitsPerEm} ${unitsPerEm}">`,
|
|
61
|
+
` <path d="${path.toPathData()}"/>`,
|
|
62
|
+
`</svg>`,
|
|
63
|
+
].join("\n");
|
|
64
|
+
|
|
65
|
+
result.set(g.unicode, svg);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API 路由
|
|
3
|
+
* 处理 iconfont 读写、SVG 上传、字体构建等请求
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import express from "express";
|
|
9
|
+
import { optimize } from "svgo";
|
|
10
|
+
import { buildCSS } from "../lib/css-generator.js";
|
|
11
|
+
import { extractGlyphToSvg, extractAllGlyphsToSvg } from "../lib/glyph-extractor.js";
|
|
12
|
+
import { buildTtfFromIcons, buildWebFonts } from "../lib/font-builder.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 创建 API 路由
|
|
16
|
+
* @param {string} iconfontDir - iconfont 目录路径
|
|
17
|
+
* @returns {express.Router}
|
|
18
|
+
*/
|
|
19
|
+
export async function createApiRouter(iconfontDir) {
|
|
20
|
+
const router = express.Router();
|
|
21
|
+
|
|
22
|
+
// 文件路径辅助
|
|
23
|
+
const filePath = (name) => path.join(iconfontDir, name);
|
|
24
|
+
|
|
25
|
+
// GET /api/read - 读取 iconfont.json
|
|
26
|
+
router.get("/read", async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const content = await fs.readFile(filePath("iconfont.json"), "utf-8");
|
|
29
|
+
res.json(JSON.parse(content));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
res.status(500).json({ error: `读取 iconfont.json 失败: ${err.message}` });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// GET /api/font - 读取字体文件,支持 ?format=woff2|woff|ttf(保留用于动态加载)
|
|
36
|
+
router.get("/font", async (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const format = req.query.format;
|
|
39
|
+
const searchOrder = format ? [format] : ["woff2", "woff", "ttf"];
|
|
40
|
+
const mimeMap = {
|
|
41
|
+
woff2: "font/woff2",
|
|
42
|
+
woff: "font/woff",
|
|
43
|
+
ttf: "font/ttf",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (const ext of searchOrder) {
|
|
47
|
+
const p = filePath(`iconfont.${ext}`);
|
|
48
|
+
try {
|
|
49
|
+
const buf = await fs.readFile(p);
|
|
50
|
+
res.setHeader("Content-Type", mimeMap[ext] || "application/octet-stream");
|
|
51
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
52
|
+
res.send(buf);
|
|
53
|
+
return;
|
|
54
|
+
} catch {
|
|
55
|
+
if (format) {
|
|
56
|
+
// 指定了格式但找不到,返回 404
|
|
57
|
+
return res.status(404).json({ error: `未找到 iconfont.${ext}` });
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
res.status(404).json({ error: "未找到字体文件" });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
res.status(500).json({ error: err.message });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// POST /api/save - 保存配置
|
|
69
|
+
router.post("/save", async (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const { json: iconJson, generateFont } = req.body;
|
|
72
|
+
|
|
73
|
+
// 保存 iconfont.json
|
|
74
|
+
await fs.writeFile(
|
|
75
|
+
filePath("iconfont.json"),
|
|
76
|
+
JSON.stringify(iconJson, null, 2),
|
|
77
|
+
"utf-8"
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// 生成并保存 CSS
|
|
81
|
+
await fs.writeFile(
|
|
82
|
+
filePath("iconfont.css"),
|
|
83
|
+
buildCSS(iconJson),
|
|
84
|
+
"utf-8"
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// 重建字体文件
|
|
88
|
+
if (generateFont) {
|
|
89
|
+
await rebuildFont(iconfontDir, iconJson);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
res.json({ success: true });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error("保存失败:", err);
|
|
95
|
+
res.status(500).json({ error: err.message });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// POST /api/upload-svg - 上传 SVG 添加新图标
|
|
100
|
+
router.post("/upload-svg", async (req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
const { svg: rawSvg, name, fontClass } = req.body;
|
|
103
|
+
|
|
104
|
+
if (!rawSvg || !rawSvg.includes("<svg")) {
|
|
105
|
+
return res.status(400).json({ error: "无效的 SVG 内容" });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// svgo 优化
|
|
109
|
+
const result = optimize(rawSvg, {
|
|
110
|
+
plugins: [
|
|
111
|
+
"removeDoctype",
|
|
112
|
+
"removeXMLProcInst",
|
|
113
|
+
"removeComments",
|
|
114
|
+
"removeMetadata",
|
|
115
|
+
"removeEditorsNSData",
|
|
116
|
+
"cleanupAttrs",
|
|
117
|
+
"removeStyleElement",
|
|
118
|
+
"removeEmptyContainers",
|
|
119
|
+
"convertShapeToPath",
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
const svg = result.data;
|
|
123
|
+
|
|
124
|
+
// 读取当前 iconfont.json
|
|
125
|
+
const currentJson = JSON.parse(
|
|
126
|
+
await fs.readFile(filePath("iconfont.json"), "utf-8")
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// 分配 unicode
|
|
130
|
+
const unicode = nextUnicode(currentJson.glyphs);
|
|
131
|
+
const iconId = `upload_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
132
|
+
|
|
133
|
+
// 添加新图标到 glyphs
|
|
134
|
+
const newGlyph = {
|
|
135
|
+
icon_id: iconId,
|
|
136
|
+
name: name || fontClass || "new-icon",
|
|
137
|
+
font_class: fontClass || `icon-${unicode}`,
|
|
138
|
+
unicode,
|
|
139
|
+
unicode_decimal: parseInt(unicode, 16),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
currentJson.glyphs.push(newGlyph);
|
|
143
|
+
|
|
144
|
+
// 读取当前 TTF,提取已有字形为 SVG
|
|
145
|
+
let existingSvgs = [];
|
|
146
|
+
try {
|
|
147
|
+
const ttfBuf = await fs.readFile(filePath("iconfont.ttf"));
|
|
148
|
+
const svgMap = extractAllGlyphsToSvg(ttfBuf, currentJson.glyphs.slice(0, -1));
|
|
149
|
+
existingSvgs = [...svgMap.entries()].map(([unicode, svg]) => ({
|
|
150
|
+
unicode,
|
|
151
|
+
svg,
|
|
152
|
+
name: currentJson.glyphs.find((g) => g.unicode === unicode)?.name || unicode,
|
|
153
|
+
}));
|
|
154
|
+
} catch {
|
|
155
|
+
// 没有 TTF 文件,跳过已有字形提取
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 合并:已有 + 新上传
|
|
159
|
+
const allIcons = [
|
|
160
|
+
...existingSvgs,
|
|
161
|
+
{ unicode, svg, name: newGlyph.name },
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
// 重建字体
|
|
165
|
+
const ttfBuffer = await buildTtfFromIcons(allIcons, {
|
|
166
|
+
fontFamily: currentJson.font_family || "iconfont",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// 写入 TTF
|
|
170
|
+
await fs.writeFile(filePath("iconfont.ttf"), ttfBuffer);
|
|
171
|
+
|
|
172
|
+
// 生成 WOFF/WOFF2
|
|
173
|
+
const webFonts = await buildWebFonts(ttfBuffer, currentJson.glyphs);
|
|
174
|
+
await fs.writeFile(filePath("iconfont.woff"), webFonts.woff);
|
|
175
|
+
await fs.writeFile(filePath("iconfont.woff2"), webFonts.woff2);
|
|
176
|
+
|
|
177
|
+
// 保存 json 和 css
|
|
178
|
+
await fs.writeFile(
|
|
179
|
+
filePath("iconfont.json"),
|
|
180
|
+
JSON.stringify(currentJson, null, 2),
|
|
181
|
+
"utf-8"
|
|
182
|
+
);
|
|
183
|
+
await fs.writeFile(
|
|
184
|
+
filePath("iconfont.css"),
|
|
185
|
+
buildCSS(currentJson),
|
|
186
|
+
"utf-8"
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
res.json({ success: true, glyph: newGlyph, json: currentJson });
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error("上传 SVG 失败:", err);
|
|
192
|
+
res.status(500).json({ error: err.message });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// GET /api/export-svg/:unicode - 导出单个图标为 SVG
|
|
197
|
+
router.get("/export-svg/:unicode", async (req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
const { unicode } = req.params;
|
|
200
|
+
const ttfBuf = await fs.readFile(filePath("iconfont.ttf"));
|
|
201
|
+
const svg = extractGlyphToSvg(ttfBuf, unicode);
|
|
202
|
+
|
|
203
|
+
res.setHeader("Content-Type", "image/svg+xml");
|
|
204
|
+
res.setHeader(
|
|
205
|
+
"Content-Disposition",
|
|
206
|
+
`attachment; filename="icon-${unicode}.svg"`
|
|
207
|
+
);
|
|
208
|
+
res.send(svg);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
res.status(500).json({ error: err.message });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return router;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 分配下一个可用的 PUA unicode(E000-F8FF 区段)
|
|
219
|
+
*/
|
|
220
|
+
function nextUnicode(glyphs) {
|
|
221
|
+
const usedCodes = new Set(
|
|
222
|
+
glyphs.map((g) => parseInt(g.unicode, 16))
|
|
223
|
+
);
|
|
224
|
+
for (let code = 0xe000; code <= 0xf8ff; code++) {
|
|
225
|
+
if (!usedCodes.has(code)) return code.toString(16);
|
|
226
|
+
}
|
|
227
|
+
throw new Error("Unicode PUA 区段已满");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 从当前字形重建字体文件
|
|
232
|
+
*/
|
|
233
|
+
async function rebuildFont(dir, iconJson) {
|
|
234
|
+
const ttfPath = path.join(dir, "iconfont.ttf");
|
|
235
|
+
|
|
236
|
+
// 读取当前 TTF,提取所有字形
|
|
237
|
+
const ttfBuf = await fs.readFile(ttfPath);
|
|
238
|
+
const svgMap = extractAllGlyphsToSvg(ttfBuf, iconJson.glyphs);
|
|
239
|
+
|
|
240
|
+
const icons = [...svgMap.entries()].map(([unicode, svg]) => ({
|
|
241
|
+
unicode,
|
|
242
|
+
svg,
|
|
243
|
+
name: iconJson.glyphs.find((g) => g.unicode === unicode)?.name || unicode,
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
// 重建 TTF
|
|
247
|
+
const newTtf = await buildTtfFromIcons(icons, {
|
|
248
|
+
fontFamily: iconJson.font_family || "iconfont",
|
|
249
|
+
});
|
|
250
|
+
await fs.writeFile(ttfPath, newTtf);
|
|
251
|
+
|
|
252
|
+
// 生成 WOFF/WOFF2
|
|
253
|
+
const webFonts = await buildWebFonts(newTtf, iconJson.glyphs);
|
|
254
|
+
await fs.writeFile(path.join(dir, "iconfont.woff"), webFonts.woff);
|
|
255
|
+
await fs.writeFile(path.join(dir, "iconfont.woff2"), webFonts.woff2);
|
|
256
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express 服务器
|
|
3
|
+
* 启动 HTTP 服务,提供 API 和前端静态文件
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import express from "express";
|
|
7
|
+
import { resolve, dirname } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { promises as fs } from "fs";
|
|
10
|
+
import { createApiRouter } from "./api.js";
|
|
11
|
+
import open from "open";
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 创建并启动服务器
|
|
17
|
+
* @param {{ dir: string, port: number }} options
|
|
18
|
+
*/
|
|
19
|
+
export async function createServer (options) {
|
|
20
|
+
const { dir, port } = options;
|
|
21
|
+
const app = express();
|
|
22
|
+
|
|
23
|
+
// 解析 JSON body
|
|
24
|
+
app.use(express.json({ limit: "10mb" }));
|
|
25
|
+
app.use(express.text({ type: "text/plain", limit: "10mb" }));
|
|
26
|
+
|
|
27
|
+
// 静态文件:iconfont 目录挂载到 /public
|
|
28
|
+
app.use(express.static(dir));
|
|
29
|
+
|
|
30
|
+
// /css - 读取 iconfont.css
|
|
31
|
+
app.get("/css", async (_req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const content = await fs.readFile(`${dir}/iconfont.css`, "utf-8");
|
|
34
|
+
res.setHeader("Content-Type", "text/css");
|
|
35
|
+
res.send(content);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
res.status(500).send(`读取 iconfont.css 失败: ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// API 路由
|
|
42
|
+
const apiRouter = await createApiRouter(dir);
|
|
43
|
+
app.use("/api", apiRouter);
|
|
44
|
+
|
|
45
|
+
// 静态文件(前端构建产物)
|
|
46
|
+
const distPath = resolve(__dirname, "../dist");
|
|
47
|
+
app.use(express.static(distPath));
|
|
48
|
+
|
|
49
|
+
// SPA 回退
|
|
50
|
+
app.get("*", (_req, res) => {
|
|
51
|
+
res.sendFile(resolve(distPath, "index.html"));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// 启动
|
|
55
|
+
app.listen(port, () => {
|
|
56
|
+
const url = `http://localhost:${port}`;
|
|
57
|
+
console.log(`\n iconfont-editor 已启动`);
|
|
58
|
+
console.log(` 目录: ${dir}`);
|
|
59
|
+
console.log(` 地址: ${url}\n`);
|
|
60
|
+
open(url).catch(() => {
|
|
61
|
+
console.log(" 请手动打开浏览器访问上述地址");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
File without changes
|