@ubean/icon 0.1.2 → 0.1.3

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/dist/vite.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { i as IconifyCollection, l as UbeanIconOptions } from "./types-CVT8GSd7.js";
2
+ import { n as ubeanIconPlugin, t as addIconCollection } from "./vite-S6Ivh3te.js";
3
+ export { type IconifyCollection, type UbeanIconOptions, addIconCollection, ubeanIconPlugin };
package/dist/vite.js ADDED
@@ -0,0 +1,318 @@
1
+ import { f as parseIconName, h as registerCollectionLoader, m as registerCollection, n as createCollectionFromSvgMap, p as parseSvgToIconData, t as clearCollections, v as scanVueSfcForIcons } from "./core-NcYHbOE5.js";
2
+ import { createRequire } from "node:module";
3
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
4
+ import { defu } from "defu";
5
+ import { basename, dirname, join, resolve } from "pathe";
6
+ //#region \0rolldown/runtime.js
7
+ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
8
+ //#endregion
9
+ //#region src/vite.ts
10
+ const VIRTUAL_MODULE_ID = "virtual:ubean-icon";
11
+ const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
12
+ const defaultOptions = {
13
+ fallbackToApi: true,
14
+ iconApiEndpoint: "https://api.iconify.design",
15
+ ssr: true,
16
+ cssSelectorPrefix: "i-",
17
+ cssWherePseudo: true,
18
+ iconifyApiEnabled: true
19
+ };
20
+ function resolveCustomCollections(rootDir, customCollections) {
21
+ const result = {};
22
+ if (!customCollections) return result;
23
+ for (const [key, config] of Object.entries(customCollections)) if (typeof config === "string") result[key] = {
24
+ prefix: key,
25
+ dir: resolve(rootDir, config),
26
+ normalizeIconName: (name) => name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")
27
+ };
28
+ else result[key] = {
29
+ prefix: config.prefix || key,
30
+ dir: resolve(rootDir, config.dir),
31
+ normalizeIconName: config.normalizeIconName || ((name) => name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, ""))
32
+ };
33
+ return result;
34
+ }
35
+ function scanSvgDirectory(dir, normalizeFn, prefixPath = "") {
36
+ const icons = {};
37
+ if (!existsSync(dir)) return icons;
38
+ const entries = readdirSync(dir);
39
+ for (const entry of entries) {
40
+ const fullPath = join(dir, entry);
41
+ const stat = statSync(fullPath);
42
+ if (stat.isDirectory()) {
43
+ if (entry.startsWith(".") || entry === "node_modules") continue;
44
+ const subIcons = scanSvgDirectory(fullPath, normalizeFn, prefixPath ? `${prefixPath}-${entry}` : entry);
45
+ Object.assign(icons, subIcons);
46
+ } else if (stat.isFile() && /\.svg$/i.test(entry)) {
47
+ const name = basename(entry, ".svg");
48
+ const iconName = normalizeFn(prefixPath ? `${prefixPath}-${name}` : name);
49
+ if (iconName) try {
50
+ icons[iconName] = readFileSync(fullPath, "utf-8");
51
+ } catch {}
52
+ }
53
+ }
54
+ return icons;
55
+ }
56
+ function loadCustomCollections(resolvedCustoms) {
57
+ const result = [];
58
+ for (const [, config] of Object.entries(resolvedCustoms)) {
59
+ if (!existsSync(config.dir)) continue;
60
+ const svgMap = scanSvgDirectory(config.dir, config.normalizeIconName);
61
+ if (Object.keys(svgMap).length > 0) {
62
+ const collection = createCollectionFromSvgMap(config.prefix, svgMap);
63
+ result.push({
64
+ prefix: config.prefix,
65
+ collection
66
+ });
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+ function serveSvgFromCustomCollection(urlPath, resolvedCustoms) {
72
+ const match = urlPath.match(/^\/([^/]+)\/(.+)\.svg$/);
73
+ if (!match) return null;
74
+ const [, prefix, iconName] = match;
75
+ for (const [, config] of Object.entries(resolvedCustoms)) {
76
+ if (config.prefix !== prefix) continue;
77
+ if (!existsSync(config.dir)) continue;
78
+ const iconFile = findSvgFile(config.dir, iconName, config.normalizeIconName);
79
+ if (iconFile) {
80
+ const data = parseSvgToIconData(readFileSync(iconFile, "utf-8"));
81
+ if (data) {
82
+ const width = data.width || 24;
83
+ const height = data.height || 24;
84
+ return {
85
+ contentType: "image/svg+xml",
86
+ content: `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${data.viewBox || `0 0 ${width} ${height}`}" fill="currentColor">${data.body}</svg>`
87
+ };
88
+ }
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ function findSvgFile(dir, iconName, normalizeFn) {
94
+ if (!existsSync(dir)) return null;
95
+ const entries = readdirSync(dir);
96
+ for (const entry of entries) {
97
+ const fullPath = join(dir, entry);
98
+ const stat = statSync(fullPath);
99
+ if (stat.isDirectory()) {
100
+ if (entry.startsWith(".") || entry === "node_modules") continue;
101
+ const normalizedDir = normalizeFn(entry);
102
+ if (iconName.startsWith(`${normalizedDir}-`)) {
103
+ const found = findSvgFile(fullPath, iconName.slice(normalizedDir.length + 1), normalizeFn);
104
+ if (found) return found;
105
+ }
106
+ } else if (stat.isFile() && /\.svg$/i.test(entry)) {
107
+ if (normalizeFn(basename(entry, ".svg")) === iconName) return fullPath;
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+ function ubeanIconPlugin(userOptions = {}) {
113
+ const options = defu(userOptions, defaultOptions);
114
+ const scannedIcons = /* @__PURE__ */ new Set();
115
+ const resolvedCollectionPaths = /* @__PURE__ */ new Map();
116
+ const customCollectionsCache = /* @__PURE__ */ new Map();
117
+ let isBuild = false;
118
+ let rootDir = "";
119
+ let resolvedCustoms = {};
120
+ function resolveCollectionPath(prefix) {
121
+ if (resolvedCollectionPaths.has(prefix)) return resolvedCollectionPaths.get(prefix);
122
+ const pkgName = `@iconify-json/${prefix}`;
123
+ try {
124
+ const path = dirname(__require.resolve(`${pkgName}/icons.json`, { paths: [rootDir] }));
125
+ resolvedCollectionPaths.set(prefix, path);
126
+ return path;
127
+ } catch {
128
+ resolvedCollectionPaths.set(prefix, null);
129
+ return null;
130
+ }
131
+ }
132
+ function loadCollectionDataSync(prefix) {
133
+ if (customCollectionsCache.has(prefix)) return customCollectionsCache.get(prefix);
134
+ const path = resolveCollectionPath(prefix);
135
+ if (!path) return null;
136
+ try {
137
+ const content = readFileSync(join(path, "icons.json"), "utf-8");
138
+ return JSON.parse(content);
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ function scanSourceForIcons(source, id) {
144
+ if (id.includes("node_modules")) return;
145
+ if (!/\.(vue|tsx?|jsx?)$/.test(id)) return;
146
+ const icons = scanVueSfcForIcons(source);
147
+ for (const icon of icons) scannedIcons.add(icon);
148
+ }
149
+ function refreshCustomCollections() {
150
+ for (const [prefix] of customCollectionsCache) customCollectionsCache.delete(prefix);
151
+ const customs = loadCustomCollections(resolvedCustoms);
152
+ for (const { prefix, collection } of customs) {
153
+ customCollectionsCache.set(prefix, collection);
154
+ registerCollection(collection);
155
+ }
156
+ }
157
+ function generateVirtualModule() {
158
+ const collectionsToRegister = [];
159
+ for (const [prefix, collection] of Object.entries(options.collections)) {
160
+ if (typeof collection === "function") continue;
161
+ collectionsToRegister.push({
162
+ prefix,
163
+ data: collection
164
+ });
165
+ }
166
+ for (const [prefix, collection] of customCollectionsCache) if (!collectionsToRegister.some((c) => c.prefix === prefix)) collectionsToRegister.push({
167
+ prefix,
168
+ data: collection
169
+ });
170
+ if (isBuild) for (const iconName of scannedIcons) {
171
+ const parsed = parseIconName(iconName);
172
+ if (!parsed) continue;
173
+ if (collectionsToRegister.some((c) => c.prefix === parsed.collection)) continue;
174
+ const data = loadCollectionDataSync(parsed.collection);
175
+ if (data) collectionsToRegister.push({
176
+ prefix: parsed.collection,
177
+ data
178
+ });
179
+ }
180
+ const collectionsJson = JSON.stringify(Object.fromEntries(collectionsToRegister.map((c) => [c.prefix, c.data])));
181
+ const loaderPrefixes = /* @__PURE__ */ new Set();
182
+ for (const [prefix, collection] of Object.entries(options.collections)) if (typeof collection === "function") loaderPrefixes.add(prefix);
183
+ for (const iconName of scannedIcons) {
184
+ const parsed = parseIconName(iconName);
185
+ if (!parsed) continue;
186
+ if (collectionsToRegister.some((c) => c.prefix === parsed.collection)) continue;
187
+ if (customCollectionsCache.has(parsed.collection)) continue;
188
+ loaderPrefixes.add(parsed.collection);
189
+ }
190
+ let loaderCode = "";
191
+ for (const prefix of loaderPrefixes) {
192
+ const importPath = resolveCollectionPath(prefix) !== null ? `@iconify-json/${prefix}/icons.json` : "";
193
+ if (importPath) loaderCode += `
194
+ registerCollectionLoader({
195
+ prefix: '${prefix}',
196
+ load: async () => {
197
+ const data = await import(/* @vite-ignore */ '${importPath}');
198
+ return data.default || data;
199
+ }
200
+ });`;
201
+ }
202
+ return `
203
+ import { registerCollection, registerCollectionLoader } from '@ubean/icon/runtime';
204
+
205
+ const collections = ${collectionsJson};
206
+ for (const [prefix, data] of Object.entries(collections)) {
207
+ registerCollection(data);
208
+ }
209
+ ${loaderCode}
210
+
211
+ export const iconOptions = ${JSON.stringify({
212
+ fallbackToApi: options.fallbackToApi,
213
+ iconApiEndpoint: options.iconApiEndpoint,
214
+ ssr: options.ssr,
215
+ iconifyApiEnabled: options.iconifyApiEnabled,
216
+ customCollectionPrefixes: Object.values(resolvedCustoms).map((c) => c.prefix)
217
+ })};
218
+
219
+ export function getScannedIcons() {
220
+ return ${JSON.stringify([...scannedIcons])};
221
+ }
222
+ `;
223
+ }
224
+ return {
225
+ name: "ubean:icon",
226
+ enforce: "pre",
227
+ configResolved(config) {
228
+ rootDir = config.root;
229
+ isBuild = config.command === "build";
230
+ resolvedCustoms = resolveCustomCollections(rootDir, options.customCollections);
231
+ clearCollections();
232
+ for (const [prefix, collection] of Object.entries(options.collections)) if (typeof collection !== "function") registerCollection(collection);
233
+ else registerCollectionLoader({
234
+ prefix,
235
+ load: collection
236
+ });
237
+ refreshCustomCollections();
238
+ },
239
+ resolveId(id) {
240
+ if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;
241
+ return null;
242
+ },
243
+ load(id) {
244
+ if (id === RESOLVED_VIRTUAL_MODULE_ID) return generateVirtualModule();
245
+ return null;
246
+ },
247
+ transform(code, id) {
248
+ scanSourceForIcons(code, id);
249
+ return null;
250
+ },
251
+ configureServer(server) {
252
+ const customDirs = Object.values(resolvedCustoms).map((c) => c.dir);
253
+ for (const dir of customDirs) if (existsSync(dir)) server.watcher.add(dir);
254
+ server.watcher.on("change", (file) => {
255
+ if (customDirs.some((dir) => file.startsWith(dir)) || /\.(vue|tsx?|jsx?)$/.test(file)) {
256
+ if (file.endsWith(".svg")) refreshCustomCollections();
257
+ const module = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID);
258
+ if (module) server.moduleGraph.invalidateModule(module);
259
+ server.ws.send({ type: "full-reload" });
260
+ }
261
+ });
262
+ server.watcher.on("add", (file) => {
263
+ if (file.endsWith(".svg") && customDirs.some((dir) => file.startsWith(dir))) {
264
+ refreshCustomCollections();
265
+ const module = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID);
266
+ if (module) server.moduleGraph.invalidateModule(module);
267
+ server.ws.send({ type: "full-reload" });
268
+ }
269
+ });
270
+ server.watcher.on("unlink", (file) => {
271
+ if (file.endsWith(".svg") && customDirs.some((dir) => file.startsWith(dir))) {
272
+ refreshCustomCollections();
273
+ const module = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID);
274
+ if (module) server.moduleGraph.invalidateModule(module);
275
+ server.ws.send({ type: "full-reload" });
276
+ }
277
+ });
278
+ if (options.iconifyApiEnabled && options.fallbackToApi) {
279
+ const iconifyHandler = async (req, res, next) => {
280
+ if (!req.url) return next();
281
+ try {
282
+ const targetPath = new URL(req.url, "http://localhost").pathname.replace(/^\//, "");
283
+ const customResult = serveSvgFromCustomCollection(`/${targetPath}`, resolvedCustoms);
284
+ if (customResult) {
285
+ res.setHeader("Content-Type", customResult.contentType);
286
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
287
+ res.statusCode = 200;
288
+ res.end(customResult.content);
289
+ return;
290
+ }
291
+ const match = targetPath.match(/^([^/]+)\/(.+)\.svg$/);
292
+ if (!match) return next();
293
+ const [, prefix, icon] = match;
294
+ const apiUrl = `${options.iconApiEndpoint}/${prefix}/${icon}.svg`;
295
+ const apiRes = await fetch(apiUrl);
296
+ if (!apiRes.ok) return next();
297
+ const svg = await apiRes.text();
298
+ res.setHeader("Content-Type", "image/svg+xml");
299
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
300
+ res.statusCode = 200;
301
+ res.end(svg);
302
+ } catch {
303
+ next();
304
+ }
305
+ };
306
+ server.middlewares.use("/_iconify", (req, res, next) => {
307
+ Promise.resolve(iconifyHandler(req, res, next)).catch(next);
308
+ });
309
+ }
310
+ }
311
+ };
312
+ }
313
+ function addIconCollection(pluginOptions, prefix, collection) {
314
+ pluginOptions.collections = pluginOptions.collections || {};
315
+ pluginOptions.collections[prefix] = collection;
316
+ }
317
+ //#endregion
318
+ export { addIconCollection, ubeanIconPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/icon",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Icon module for ubean with Iconify integration",
5
5
  "files": [
6
6
  "dist"