@moewolf/moe-icons-cli 0.0.1
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 +15 -0
- package/README.md +32 -0
- package/bin/moeicons.js +20 -0
- package/dist/api/client.d.ts +42 -0
- package/dist/api/client.js +105 -0
- package/dist/auth/callback.d.ts +37 -0
- package/dist/auth/callback.js +72 -0
- package/dist/auth/device-login.d.ts +29 -0
- package/dist/auth/device-login.js +74 -0
- package/dist/auth/login.d.ts +41 -0
- package/dist/auth/login.js +85 -0
- package/dist/auth/open-browser.d.ts +2 -0
- package/dist/auth/open-browser.js +15 -0
- package/dist/auth/pkce.d.ts +17 -0
- package/dist/auth/pkce.js +20 -0
- package/dist/auth/token-store.d.ts +38 -0
- package/dist/auth/token-store.js +126 -0
- package/dist/catalog/catalog.d.ts +32 -0
- package/dist/catalog/catalog.js +80 -0
- package/dist/catalog/catalog.json +9371 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.js +507 -0
- package/dist/commands/parser.d.ts +41 -0
- package/dist/commands/parser.js +146 -0
- package/dist/core/auth.d.ts +32 -0
- package/dist/core/auth.js +131 -0
- package/dist/core/bitmap-assets.d.ts +29 -0
- package/dist/core/bitmap-assets.js +77 -0
- package/dist/core/cli-update.d.ts +43 -0
- package/dist/core/cli-update.js +71 -0
- package/dist/core/context.d.ts +22 -0
- package/dist/core/context.js +1 -0
- package/dist/core/free-download.d.ts +66 -0
- package/dist/core/free-download.js +193 -0
- package/dist/core/generate.d.ts +33 -0
- package/dist/core/generate.js +285 -0
- package/dist/core/icon-names.d.ts +14 -0
- package/dist/core/icon-names.js +54 -0
- package/dist/core/init.d.ts +12 -0
- package/dist/core/init.js +23 -0
- package/dist/core/install.d.ts +67 -0
- package/dist/core/install.js +117 -0
- package/dist/core/library-update.d.ts +24 -0
- package/dist/core/library-update.js +161 -0
- package/dist/core/manage.d.ts +24 -0
- package/dist/core/manage.js +38 -0
- package/dist/core/pro-download.d.ts +29 -0
- package/dist/core/pro-download.js +112 -0
- package/dist/core/pro-install.d.ts +24 -0
- package/dist/core/pro-install.js +72 -0
- package/dist/core/release-descriptor.d.ts +40 -0
- package/dist/core/release-descriptor.js +114 -0
- package/dist/core/resource-variant.d.ts +35 -0
- package/dist/core/resource-variant.js +70 -0
- package/dist/core/signed-artifact.d.ts +29 -0
- package/dist/core/signed-artifact.js +81 -0
- package/dist/core/target-subtree.d.ts +49 -0
- package/dist/core/target-subtree.js +132 -0
- package/dist/core/update-policy.d.ts +31 -0
- package/dist/core/update-policy.js +27 -0
- package/dist/core/version-service.d.ts +26 -0
- package/dist/core/version-service.js +71 -0
- package/dist/core/wizard.d.ts +49 -0
- package/dist/core/wizard.js +71 -0
- package/dist/errors/errors.d.ts +34 -0
- package/dist/errors/errors.js +39 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/generator/cn.d.ts +5 -0
- package/dist/generator/cn.js +14 -0
- package/dist/generator/generate.d.ts +34 -0
- package/dist/generator/generate.js +562 -0
- package/dist/generator/theme-resolve.d.ts +26 -0
- package/dist/generator/theme-resolve.js +100 -0
- package/dist/mcp/server.d.ts +58 -0
- package/dist/mcp/server.js +176 -0
- package/dist/project/config.d.ts +74 -0
- package/dist/project/config.js +353 -0
- package/dist/project/detect.d.ts +26 -0
- package/dist/project/detect.js +104 -0
- package/dist/project/install-metadata.d.ts +29 -0
- package/dist/project/install-metadata.js +108 -0
- package/dist/project/install.d.ts +115 -0
- package/dist/project/install.js +537 -0
- package/dist/project/project-lock.d.ts +11 -0
- package/dist/project/project-lock.js +81 -0
- package/dist/project/tailwind.d.ts +51 -0
- package/dist/project/tailwind.js +188 -0
- package/dist/project/tar-gz.d.ts +12 -0
- package/dist/project/tar-gz.js +100 -0
- package/dist/tui/primitives.d.ts +36 -0
- package/dist/tui/primitives.js +88 -0
- package/dist/ui/banner.d.ts +11 -0
- package/dist/ui/banner.js +105 -0
- package/dist/ui/clack.d.ts +8 -0
- package/dist/ui/clack.js +53 -0
- package/dist/ui/create-ui.d.ts +14 -0
- package/dist/ui/create-ui.js +16 -0
- package/dist/ui/non-interactive.d.ts +5 -0
- package/dist/ui/non-interactive.js +30 -0
- package/dist/ui/stream.d.ts +11 -0
- package/dist/ui/stream.js +50 -0
- package/package.json +70 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { catalog, findCatalogIcon } from "../catalog/catalog.js";
|
|
2
|
+
import { reportLibraryExportNameCollisions, reportProxyNameCollisions, toLibraryExportName, toProxyName, } from "../core/icon-names.js";
|
|
3
|
+
import { selectBitmapVariantAssets } from "../core/bitmap-assets.js";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { decodeUtf8 } from "../project/tar-gz.js";
|
|
6
|
+
import { CN_HELPER_SOURCE } from "./cn.js";
|
|
7
|
+
import { bitmapWrapperImportName, reactBitmapWrapperSource, resolveThemes, svgInternalImportName, vueBitmapWrapperSource, } from "./theme-resolve.js";
|
|
8
|
+
export { toLibraryExportName, toProxyName };
|
|
9
|
+
/** @deprecated Use toProxyName; kept so existing generator tests keep compiling. */
|
|
10
|
+
export const toPascalCase = toProxyName;
|
|
11
|
+
const OWNER_HEADER = "// Generated by moeicons CLI. Do not edit manually; re-run `moeicons generate`.";
|
|
12
|
+
function themeClassExpr(themes) {
|
|
13
|
+
const entries = themes
|
|
14
|
+
.map((theme) => {
|
|
15
|
+
const className = theme.entry.className;
|
|
16
|
+
return ` ${JSON.stringify(theme.theme)}: ${className === undefined ? "undefined" : JSON.stringify(className)},`;
|
|
17
|
+
})
|
|
18
|
+
.join("\n");
|
|
19
|
+
return `const themeClassName: Partial<Record<Theme, string | undefined>> = {\n${entries}\n};\n`;
|
|
20
|
+
}
|
|
21
|
+
function appendReactFiles(files, rel, config, themes) {
|
|
22
|
+
files.push({
|
|
23
|
+
path: rel("cn.ts"),
|
|
24
|
+
content: CN_HELPER_SOURCE,
|
|
25
|
+
});
|
|
26
|
+
files.push({
|
|
27
|
+
path: rel("types.ts"),
|
|
28
|
+
content: `${OWNER_HEADER}
|
|
29
|
+
export interface IconProps {
|
|
30
|
+
className?: string;
|
|
31
|
+
size?: number;
|
|
32
|
+
strokeWidth?: number;
|
|
33
|
+
"aria-label"?: string;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
export type Theme =
|
|
37
|
+
${Object.keys(config.themes)
|
|
38
|
+
.map((theme) => ` | ${JSON.stringify(theme)}`)
|
|
39
|
+
.join("\n")};
|
|
40
|
+
`,
|
|
41
|
+
});
|
|
42
|
+
const importLines = [];
|
|
43
|
+
for (const resolved of themes) {
|
|
44
|
+
if (resolved.kind === "svg") {
|
|
45
|
+
for (const iconId of config.icons) {
|
|
46
|
+
const alias = svgInternalImportName(resolved.theme, resolved.entry.styleGroup, iconId);
|
|
47
|
+
importLines.push(`import { ${toLibraryExportName(iconId)} as ${alias} } from "moe-icons/${config.tier}/react/${resolved.entry.styleGroup}";`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (resolved.variant) {
|
|
51
|
+
for (const iconId of config.icons) {
|
|
52
|
+
const wrapperName = bitmapWrapperImportName(resolved.theme, iconId);
|
|
53
|
+
const wrapperPath = `./wrappers/${wrapperName}`;
|
|
54
|
+
importLines.push(`import { ${wrapperName} } from "${wrapperPath}";`);
|
|
55
|
+
files.push({
|
|
56
|
+
path: rel(`wrappers/${wrapperName}.tsx`),
|
|
57
|
+
content: `${OWNER_HEADER}
|
|
58
|
+
${reactBitmapWrapperSource(iconId, wrapperName, resolved.variant, resolved.entry.className)}`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
files.push({
|
|
64
|
+
path: rel("registry.ts"),
|
|
65
|
+
content: `${OWNER_HEADER}
|
|
66
|
+
import type { IconProps, Theme } from "./types";
|
|
67
|
+
import type * as React from "react";
|
|
68
|
+
${importLines.join("\n")}
|
|
69
|
+
|
|
70
|
+
export type IconId =
|
|
71
|
+
${config.icons.map((id) => ` | "${id}"`).join("\n")};
|
|
72
|
+
|
|
73
|
+
export interface ThemeRegistry {
|
|
74
|
+
${config.icons.map((id) => ` readonly ${toPascalCase(id)}: React.ComponentType<IconProps>;`).join("\n")}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const registry: Record<Theme, ThemeRegistry> = {
|
|
78
|
+
${themes
|
|
79
|
+
.map((resolved) => {
|
|
80
|
+
const entries = config.icons
|
|
81
|
+
.map((id) => {
|
|
82
|
+
const component = resolved.kind === "bitmap"
|
|
83
|
+
? bitmapWrapperImportName(resolved.theme, id)
|
|
84
|
+
: svgInternalImportName(resolved.theme, resolved.entry.styleGroup, id);
|
|
85
|
+
return ` ${toPascalCase(id)}: ${component},`;
|
|
86
|
+
})
|
|
87
|
+
.join("\n");
|
|
88
|
+
return ` "${resolved.theme}": {\n${entries}\n },`;
|
|
89
|
+
})
|
|
90
|
+
.join("\n")}
|
|
91
|
+
};
|
|
92
|
+
`,
|
|
93
|
+
});
|
|
94
|
+
files.push({
|
|
95
|
+
path: rel("provider.tsx"),
|
|
96
|
+
content: `${OWNER_HEADER}
|
|
97
|
+
import { createContext, useContext, useState, type PropsWithChildren } from "react";
|
|
98
|
+
import type { Theme } from "./types";
|
|
99
|
+
|
|
100
|
+
interface MoeiconsThemeState { readonly theme: Theme; readonly setTheme: (theme: Theme) => void; }
|
|
101
|
+
const MoeiconsThemeContext = createContext<MoeiconsThemeState | undefined>(undefined);
|
|
102
|
+
export function MoeiconsProvider(props: PropsWithChildren<{ theme?: Theme }>) {
|
|
103
|
+
const [theme, setTheme] = useState<Theme>(props.theme ?? ${JSON.stringify(config.defaultTheme)});
|
|
104
|
+
return <MoeiconsThemeContext.Provider value={{ theme, setTheme }}>{props.children}</MoeiconsThemeContext.Provider>;
|
|
105
|
+
}
|
|
106
|
+
export function useMoeiconsTheme(): MoeiconsThemeState {
|
|
107
|
+
const state = useContext(MoeiconsThemeContext);
|
|
108
|
+
if (!state) throw new Error("useMoeiconsTheme must be used inside <MoeiconsProvider>");
|
|
109
|
+
return state;
|
|
110
|
+
}
|
|
111
|
+
`,
|
|
112
|
+
});
|
|
113
|
+
for (const iconId of config.icons) {
|
|
114
|
+
const pascal = toPascalCase(iconId);
|
|
115
|
+
files.push({
|
|
116
|
+
path: rel(`icons/${pascal}.tsx`),
|
|
117
|
+
content: `${OWNER_HEADER}
|
|
118
|
+
import type { IconProps } from "../types";
|
|
119
|
+
import { registry } from "../registry";
|
|
120
|
+
import { useMoeiconsTheme } from "../provider";
|
|
121
|
+
import { cn } from "../cn";
|
|
122
|
+
import type { Theme } from "../types";
|
|
123
|
+
|
|
124
|
+
${themeClassExpr(themes)}
|
|
125
|
+
|
|
126
|
+
export function ${pascal}(props: IconProps) {
|
|
127
|
+
const { className, size, ...rest } = props;
|
|
128
|
+
const { theme } = useMoeiconsTheme();
|
|
129
|
+
const Component = registry[theme]?.${pascal} ?? registry[${JSON.stringify(config.defaultTheme)}].${pascal};
|
|
130
|
+
const sizeClass = typeof size === "number" ? \`w-[\${size}px] h-[\${size}px]\` : undefined;
|
|
131
|
+
return <Component {...rest} size={size} className={cn("moe-icon", themeClassName[theme as Theme], sizeClass, className)} />;
|
|
132
|
+
}
|
|
133
|
+
`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
files.push({
|
|
137
|
+
path: rel("index.ts"),
|
|
138
|
+
content: `${OWNER_HEADER}
|
|
139
|
+
export type { IconProps } from "./types";
|
|
140
|
+
export { cn } from "./cn";
|
|
141
|
+
export { MoeiconsProvider, useMoeiconsTheme } from "./provider";
|
|
142
|
+
${config.icons.map((id) => `export { ${toPascalCase(id)} } from "./icons/${toPascalCase(id)}";`).join("\n")}
|
|
143
|
+
`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function appendVueFiles(files, rel, config, themes) {
|
|
147
|
+
files.push({
|
|
148
|
+
path: rel("cn.ts"),
|
|
149
|
+
content: CN_HELPER_SOURCE,
|
|
150
|
+
});
|
|
151
|
+
files.push({
|
|
152
|
+
path: rel("types.ts"),
|
|
153
|
+
content: `${OWNER_HEADER}
|
|
154
|
+
export interface IconProps {
|
|
155
|
+
class?: string;
|
|
156
|
+
size?: number;
|
|
157
|
+
strokeWidth?: number;
|
|
158
|
+
"aria-label"?: string;
|
|
159
|
+
[key: string]: unknown;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export type Theme =
|
|
163
|
+
${Object.keys(config.themes)
|
|
164
|
+
.map((theme) => ` | ${JSON.stringify(theme)}`)
|
|
165
|
+
.join("\n")};
|
|
166
|
+
`,
|
|
167
|
+
});
|
|
168
|
+
files.push({
|
|
169
|
+
path: rel("theme.ts"),
|
|
170
|
+
content: `${OWNER_HEADER}
|
|
171
|
+
import type { InjectionKey, Ref } from "vue";
|
|
172
|
+
import type { Theme } from "./types";
|
|
173
|
+
|
|
174
|
+
export interface MoeiconsThemeState {
|
|
175
|
+
readonly theme: Ref<Theme>;
|
|
176
|
+
readonly setTheme: (theme: Theme) => void;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const MOEICONS_THEME_KEY: InjectionKey<MoeiconsThemeState> = Symbol("moeicons-theme");
|
|
180
|
+
`,
|
|
181
|
+
});
|
|
182
|
+
const importLines = [];
|
|
183
|
+
for (const resolved of themes) {
|
|
184
|
+
if (resolved.kind === "svg") {
|
|
185
|
+
for (const iconId of config.icons) {
|
|
186
|
+
const alias = svgInternalImportName(resolved.theme, resolved.entry.styleGroup, iconId);
|
|
187
|
+
importLines.push(`import { ${toLibraryExportName(iconId)} as ${alias} } from "moe-icons/${config.tier}/vue/${resolved.entry.styleGroup}";`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (resolved.variant) {
|
|
191
|
+
for (const iconId of config.icons) {
|
|
192
|
+
const wrapperName = bitmapWrapperImportName(resolved.theme, iconId);
|
|
193
|
+
importLines.push(`import { ${wrapperName} } from "./wrappers/${wrapperName}";`);
|
|
194
|
+
files.push({
|
|
195
|
+
path: rel(`wrappers/${wrapperName}.ts`),
|
|
196
|
+
content: `${OWNER_HEADER}
|
|
197
|
+
${vueBitmapWrapperSource(iconId, wrapperName, resolved.variant, resolved.entry.className)}`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
files.push({
|
|
203
|
+
path: rel("registry.ts"),
|
|
204
|
+
content: `${OWNER_HEADER}
|
|
205
|
+
import type { Component } from "vue";
|
|
206
|
+
import type { Theme } from "./types";
|
|
207
|
+
${importLines.join("\n")}
|
|
208
|
+
|
|
209
|
+
export type IconId =
|
|
210
|
+
${config.icons.map((id) => ` | "${id}"`).join("\n")};
|
|
211
|
+
|
|
212
|
+
export interface ThemeRegistry {
|
|
213
|
+
${config.icons.map((id) => ` readonly ${toPascalCase(id)}: Component;`).join("\n")}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const registry: Record<Theme, ThemeRegistry> = {
|
|
217
|
+
${themes
|
|
218
|
+
.map((resolved) => {
|
|
219
|
+
const entries = config.icons
|
|
220
|
+
.map((id) => {
|
|
221
|
+
const component = resolved.kind === "bitmap"
|
|
222
|
+
? bitmapWrapperImportName(resolved.theme, id)
|
|
223
|
+
: svgInternalImportName(resolved.theme, resolved.entry.styleGroup, id);
|
|
224
|
+
return ` ${toPascalCase(id)}: ${component},`;
|
|
225
|
+
})
|
|
226
|
+
.join("\n");
|
|
227
|
+
return ` "${resolved.theme}": {\n${entries}\n },`;
|
|
228
|
+
})
|
|
229
|
+
.join("\n")}
|
|
230
|
+
};
|
|
231
|
+
`,
|
|
232
|
+
});
|
|
233
|
+
files.push({
|
|
234
|
+
path: rel("provider.ts"),
|
|
235
|
+
content: `${OWNER_HEADER}
|
|
236
|
+
import { defineComponent, provide, ref, type PropType } from "vue";
|
|
237
|
+
import type { Theme } from "./types";
|
|
238
|
+
import { MOEICONS_THEME_KEY, type MoeiconsThemeState } from "./theme";
|
|
239
|
+
|
|
240
|
+
export const MoeiconsProvider = defineComponent({
|
|
241
|
+
name: "MoeiconsProvider",
|
|
242
|
+
props: {
|
|
243
|
+
theme: { type: String as PropType<Theme>, default: ${JSON.stringify(config.defaultTheme)} as Theme },
|
|
244
|
+
},
|
|
245
|
+
setup(props, { slots }) {
|
|
246
|
+
const theme = ref<Theme>(props.theme);
|
|
247
|
+
const setTheme = (next: Theme) => {
|
|
248
|
+
theme.value = next;
|
|
249
|
+
};
|
|
250
|
+
const state: MoeiconsThemeState = { theme, setTheme };
|
|
251
|
+
provide(MOEICONS_THEME_KEY, state);
|
|
252
|
+
return () => slots.default?.();
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
`,
|
|
256
|
+
});
|
|
257
|
+
files.push({
|
|
258
|
+
path: rel("composable.ts"),
|
|
259
|
+
content: `${OWNER_HEADER}
|
|
260
|
+
import { inject } from "vue";
|
|
261
|
+
import { MOEICONS_THEME_KEY } from "./theme";
|
|
262
|
+
import type { MoeiconsThemeState } from "./theme";
|
|
263
|
+
|
|
264
|
+
export function useMoeiconsTheme(): MoeiconsThemeState {
|
|
265
|
+
const state = inject(MOEICONS_THEME_KEY);
|
|
266
|
+
if (!state) {
|
|
267
|
+
throw new Error("useMoeiconsTheme must be used inside <MoeiconsProvider>");
|
|
268
|
+
}
|
|
269
|
+
return state;
|
|
270
|
+
}
|
|
271
|
+
`,
|
|
272
|
+
});
|
|
273
|
+
for (const iconId of config.icons) {
|
|
274
|
+
const pascal = toPascalCase(iconId);
|
|
275
|
+
files.push({
|
|
276
|
+
path: rel(`icons/${pascal}.ts`),
|
|
277
|
+
content: `${OWNER_HEADER}
|
|
278
|
+
import { computed, defineComponent, h, inject } from "vue";
|
|
279
|
+
import { MOEICONS_THEME_KEY, type MoeiconsThemeState } from "../theme";
|
|
280
|
+
import { registry } from "../registry";
|
|
281
|
+
import { cn } from "../cn";
|
|
282
|
+
import type { Theme } from "../types";
|
|
283
|
+
|
|
284
|
+
${themeClassExpr(themes)}
|
|
285
|
+
|
|
286
|
+
export const ${pascal} = defineComponent({
|
|
287
|
+
name: "${pascal}",
|
|
288
|
+
setup(_props, { attrs }) {
|
|
289
|
+
const state = inject<MoeiconsThemeState | undefined>(MOEICONS_THEME_KEY, undefined);
|
|
290
|
+
const component = computed(() => registry[(state?.theme.value ?? ${JSON.stringify(config.defaultTheme)}) as Theme]?.${pascal} ?? registry[${JSON.stringify(config.defaultTheme)}].${pascal});
|
|
291
|
+
return () => {
|
|
292
|
+
const className = typeof attrs.class === "string" ? attrs.class : undefined;
|
|
293
|
+
const size = typeof attrs.size === "number" ? attrs.size : undefined;
|
|
294
|
+
const sizeClass = typeof size === "number" ? \`w-[\${size}px] h-[\${size}px]\` : undefined;
|
|
295
|
+
const theme = (state?.theme.value ?? ${JSON.stringify(config.defaultTheme)}) as Theme;
|
|
296
|
+
return h(component.value, {
|
|
297
|
+
...attrs,
|
|
298
|
+
class: cn("moe-icon", themeClassName[theme], sizeClass, className),
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
`,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
files.push({
|
|
307
|
+
path: rel("index.ts"),
|
|
308
|
+
content: `${OWNER_HEADER}
|
|
309
|
+
export type { IconProps } from "./types";
|
|
310
|
+
export { cn } from "./cn";
|
|
311
|
+
export { MOEICONS_THEME_KEY, type MoeiconsThemeState } from "./theme";
|
|
312
|
+
export { useMoeiconsTheme } from "./composable";
|
|
313
|
+
export { MoeiconsProvider } from "./provider";
|
|
314
|
+
${config.icons
|
|
315
|
+
.map((id) => `export { ${toPascalCase(id)} } from "./icons/${toPascalCase(id)}";`)
|
|
316
|
+
.join("\n")}
|
|
317
|
+
`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function parseSvgNodes(source) {
|
|
321
|
+
const root = { children: [] };
|
|
322
|
+
const stack = [root];
|
|
323
|
+
const svg = source.match(/<svg\b[^>]*>/i);
|
|
324
|
+
if (!svg)
|
|
325
|
+
throw new Error("SVG root element is missing");
|
|
326
|
+
const viewBox = svg[0].match(/\bviewBox\s*=\s*["']([^"']+)["']/i)?.[1] ?? "0 0 24 24";
|
|
327
|
+
const inner = source.slice((svg.index ?? 0) + svg[0].length).replace(/<\/svg>\s*$/i, "");
|
|
328
|
+
const tokenRe = /<!--[\s\S]*?-->|<[^>]+>|[^<]+/g;
|
|
329
|
+
for (const match of inner.matchAll(tokenRe)) {
|
|
330
|
+
const token = match[0];
|
|
331
|
+
if (token.startsWith("<!--") || !token.trim())
|
|
332
|
+
continue;
|
|
333
|
+
if (token.startsWith("</")) {
|
|
334
|
+
if (stack.length > 1)
|
|
335
|
+
stack.pop();
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (!token.startsWith("<")) {
|
|
339
|
+
stack[stack.length - 1]?.children.push({ text: token });
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const selfClosing = /\/\s*>$/.test(token);
|
|
343
|
+
const body = token.slice(1, token.length - (selfClosing ? 2 : 1)).trim();
|
|
344
|
+
const name = body.match(/^[A-Za-z][\w:.-]*/)?.[0];
|
|
345
|
+
if (!name)
|
|
346
|
+
continue;
|
|
347
|
+
const attrs = [];
|
|
348
|
+
body.slice(name.length).replace(/([A-Za-z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g, (_full, attr, doubleValue, singleValue, bareValue) => {
|
|
349
|
+
attrs.push([attr, doubleValue ?? singleValue ?? bareValue ?? ""]);
|
|
350
|
+
return "";
|
|
351
|
+
});
|
|
352
|
+
const node = { name, attrs, children: [] };
|
|
353
|
+
stack[stack.length - 1]?.children.push(node);
|
|
354
|
+
if (!selfClosing)
|
|
355
|
+
stack.push(node);
|
|
356
|
+
}
|
|
357
|
+
return { viewBox, children: root.children };
|
|
358
|
+
}
|
|
359
|
+
function vanillaFactorySource(name, source, strategy) {
|
|
360
|
+
const parsed = parseSvgNodes(source);
|
|
361
|
+
const paint = strategy === "solid" ? [["fill", "currentColor"]] : strategy === "outline" ? [["stroke", "currentColor"], ["fill", "none"]] : [];
|
|
362
|
+
const graphic = new Set(["path", "circle", "ellipse", "line", "polyline", "polygon", "rect", "text", "use", "image"]);
|
|
363
|
+
const lines = [
|
|
364
|
+
"const SVG_NS = 'http://www.w3.org/2000/svg';", "",
|
|
365
|
+
"export interface VanillaIconOptions extends Record<string, string | number | undefined> { className?: string; strokeWidth?: number; }", "",
|
|
366
|
+
`export function create${name}(options: VanillaIconOptions = {}): SVGElement {`,
|
|
367
|
+
" const svg = document.createElementNS(SVG_NS, 'svg');",
|
|
368
|
+
` svg.setAttribute('viewBox', ${JSON.stringify(parsed.viewBox)});`,
|
|
369
|
+
" svg.setAttribute('class', options.className ? `moe-icon ${options.className}` : 'moe-icon');",
|
|
370
|
+
];
|
|
371
|
+
for (const [attr, value] of paint)
|
|
372
|
+
lines.push(` svg.setAttribute(${JSON.stringify(attr)}, ${JSON.stringify(value)});`);
|
|
373
|
+
lines.push(" if (options.strokeWidth !== undefined) svg.setAttribute('stroke-width', String(options.strokeWidth));");
|
|
374
|
+
lines.push(" for (const [name, value] of Object.entries(options)) if (name !== 'className' && name !== 'strokeWidth' && value !== undefined) svg.setAttribute(name, String(value));");
|
|
375
|
+
let counter = 0;
|
|
376
|
+
const emit = (nodes, parent) => {
|
|
377
|
+
for (const node of nodes) {
|
|
378
|
+
if ("text" in node) {
|
|
379
|
+
lines.push(` ${parent}.appendChild(document.createTextNode(${JSON.stringify(node.text)}));`);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const variable = `node${counter++}`;
|
|
383
|
+
lines.push(` const ${variable} = document.createElementNS(SVG_NS, ${JSON.stringify(node.name)});`);
|
|
384
|
+
for (const [attr, value] of node.attrs)
|
|
385
|
+
lines.push(` ${variable}.setAttribute(${JSON.stringify(attr)}, ${JSON.stringify(value)});`);
|
|
386
|
+
if (graphic.has(node.name.toLowerCase())) {
|
|
387
|
+
if (strategy === "outline" && !node.attrs.some(([attr]) => attr.toLowerCase() === "stroke"))
|
|
388
|
+
lines.push(` ${variable}.setAttribute('stroke', 'currentColor');`);
|
|
389
|
+
if (strategy === "outline" && !node.attrs.some(([attr]) => attr.toLowerCase() === "fill"))
|
|
390
|
+
lines.push(` ${variable}.setAttribute('fill', 'none');`);
|
|
391
|
+
if (strategy === "solid" && !node.attrs.some(([attr]) => attr.toLowerCase() === "fill"))
|
|
392
|
+
lines.push(` ${variable}.setAttribute('fill', 'currentColor');`);
|
|
393
|
+
}
|
|
394
|
+
emit(node.children, variable);
|
|
395
|
+
lines.push(` ${parent}.appendChild(${variable});`);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
emit(parsed.children, "svg");
|
|
399
|
+
lines.push(" return svg;", "}", "", `export default create${name};`, "");
|
|
400
|
+
return lines.join("\n");
|
|
401
|
+
}
|
|
402
|
+
function archiveManifest(archiveFiles) {
|
|
403
|
+
const entry = Object.entries(archiveFiles).find(([path]) => /(?:^|\/)assets\/manifest\.json$/.test(path));
|
|
404
|
+
if (!entry)
|
|
405
|
+
return undefined;
|
|
406
|
+
try {
|
|
407
|
+
const parsed = JSON.parse(decodeUtf8(entry[1]));
|
|
408
|
+
if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.assets))
|
|
409
|
+
return undefined;
|
|
410
|
+
const assets = parsed.assets.filter((asset) => typeof asset.path === "string" && typeof asset.size === "number" && Number.isSafeInteger(asset.size) && asset.size >= 0 && typeof asset.sha256 === "string");
|
|
411
|
+
return { path: entry[0], assets };
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function sha256Hex(bytes) {
|
|
418
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
419
|
+
}
|
|
420
|
+
function selectRawAssets(archiveFiles, themes, icons) {
|
|
421
|
+
const manifest = archiveManifest(archiveFiles);
|
|
422
|
+
if (!manifest)
|
|
423
|
+
return { ok: false, errors: ["installed artifact assets manifest is missing or invalid"] };
|
|
424
|
+
const base = manifest.path.slice(0, manifest.path.lastIndexOf("/"));
|
|
425
|
+
const wanted = new Set(themes.filter((theme) => theme.kind === "svg").flatMap((theme) => icons.map((icon) => `${theme.entry.styleGroup}/${icon}.svg`)));
|
|
426
|
+
const assets = [];
|
|
427
|
+
const errors = [];
|
|
428
|
+
for (const path of wanted) {
|
|
429
|
+
const entry = manifest.assets.find((asset) => asset.path === path);
|
|
430
|
+
const bytes = entry ? archiveFiles[`${base}/${entry.path}`] ?? archiveFiles[entry.path] : undefined;
|
|
431
|
+
if (!entry || !bytes) {
|
|
432
|
+
errors.push(`raw asset missing: ${path}`);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const sizeOk = bytes.byteLength === entry.size;
|
|
436
|
+
const shaOk = sha256Hex(bytes) === entry.sha256.toLowerCase();
|
|
437
|
+
if (!sizeOk || !shaOk) {
|
|
438
|
+
errors.push(`raw asset verification failed for ${path}: expected size ${entry.size}/sha256 ${entry.sha256}, got size ${bytes.byteLength}/sha256 ${sha256Hex(bytes)}`);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
assets.push({ path: `assets/${entry.path}`, content: bytes });
|
|
442
|
+
}
|
|
443
|
+
if (errors.length > 0)
|
|
444
|
+
return { ok: false, errors };
|
|
445
|
+
return { ok: true, assets };
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Plan generated files for a config. Returns only owned paths under the config
|
|
449
|
+
* output dir. Rejects duplicate PascalCase names and a config with no themes.
|
|
450
|
+
* Theme keys stay logical style themes; bitmap resourceVariantId never becomes a theme.
|
|
451
|
+
*/
|
|
452
|
+
export function planGeneratedFiles(config, outputDir, options = {}) {
|
|
453
|
+
const errors = [];
|
|
454
|
+
if (Object.keys(config.themes).length === 0) {
|
|
455
|
+
errors.push("config has no themes");
|
|
456
|
+
}
|
|
457
|
+
if (config.icons.length === 0) {
|
|
458
|
+
errors.push("config has no icons");
|
|
459
|
+
}
|
|
460
|
+
errors.push(...reportProxyNameCollisions(config.icons));
|
|
461
|
+
errors.push(...reportLibraryExportNameCollisions(config.icons));
|
|
462
|
+
for (const iconId of config.icons) {
|
|
463
|
+
const catalogIcon = findCatalogIcon(iconId, options.catalog ?? catalog);
|
|
464
|
+
if (!catalogIcon) {
|
|
465
|
+
errors.push(`unknown icon "${iconId}"`);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
for (const [theme, entry] of Object.entries(config.themes)) {
|
|
469
|
+
if (!catalogIcon.availableIn.includes(entry.styleGroup)) {
|
|
470
|
+
errors.push(`icon "${iconId}" is not available in style group "${entry.styleGroup}" for theme "${theme}"`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const resolved = resolveThemes(config, options.catalog ?? catalog);
|
|
475
|
+
if (!resolved.ok)
|
|
476
|
+
errors.push(...resolved.errors);
|
|
477
|
+
if (errors.length > 0 || !resolved.ok)
|
|
478
|
+
return { ok: false, errors };
|
|
479
|
+
const files = [];
|
|
480
|
+
const rel = (p) => `${outputDir.replace(/\/$/, "")}/${p}`;
|
|
481
|
+
const target = config.target;
|
|
482
|
+
if (target === "assets") {
|
|
483
|
+
// Assets are a raw-resource target; never emit wrapper code for it.
|
|
484
|
+
}
|
|
485
|
+
else if (target === "vanilla") {
|
|
486
|
+
if (resolved.themes.some((theme) => theme.kind === "bitmap")) {
|
|
487
|
+
return { ok: false, errors: ["vanilla target supports SVG themes only"] };
|
|
488
|
+
}
|
|
489
|
+
if (options.archiveFiles === undefined) {
|
|
490
|
+
return { ok: false, errors: ["vanilla target requires an installed artifact"] };
|
|
491
|
+
}
|
|
492
|
+
const raw = selectRawAssets(options.archiveFiles, resolved.themes, config.icons);
|
|
493
|
+
if (!raw.ok)
|
|
494
|
+
return raw;
|
|
495
|
+
const rawByPath = new Map(raw.assets.map((file) => [file.path.replace(/^assets\//, ""), file.content]));
|
|
496
|
+
const groups = new Set(resolved.themes.map((theme) => theme.entry.styleGroup));
|
|
497
|
+
files.push({ path: rel("types.ts"), content: "export interface VanillaIconOptions extends Record<string, string | number | undefined> { className?: string; strokeWidth?: number; }\n" });
|
|
498
|
+
for (const styleGroup of groups) {
|
|
499
|
+
const exports = ["// Vanilla factory exports"];
|
|
500
|
+
for (const iconId of config.icons) {
|
|
501
|
+
const name = toPascalCase(iconId);
|
|
502
|
+
const source = rawByPath.get(`${styleGroup}/${iconId}.svg`);
|
|
503
|
+
if (!source || typeof source === "string")
|
|
504
|
+
return { ok: false, errors: [`raw asset missing: ${styleGroup}/${iconId}.svg`] };
|
|
505
|
+
const strategy = styleGroup.endsWith("-outline") ? "outline" : styleGroup.endsWith("-solid") ? "solid" : "mixed";
|
|
506
|
+
files.push({ path: rel(`${styleGroup}/${name}.ts`), content: vanillaFactorySource(name, decodeUtf8(source), strategy) });
|
|
507
|
+
exports.push(`export { default as ${toLibraryExportName(iconId)}, create${name} } from './${name}';`);
|
|
508
|
+
}
|
|
509
|
+
files.push({ path: rel(`${styleGroup}/index.ts`), content: `${exports.join("\n")}\n` });
|
|
510
|
+
}
|
|
511
|
+
files.push({ path: rel("index.ts"), content: `${[...groups].map((group) => `export * from './${group}';`).join("\n")}\n` });
|
|
512
|
+
}
|
|
513
|
+
else if (target === "vue") {
|
|
514
|
+
appendVueFiles(files, rel, config, resolved.themes);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
appendReactFiles(files, rel, config, resolved.themes);
|
|
518
|
+
}
|
|
519
|
+
if (target === "assets") {
|
|
520
|
+
if (options.archiveFiles === undefined) {
|
|
521
|
+
return { ok: false, errors: ["assets target requires an installed artifact"] };
|
|
522
|
+
}
|
|
523
|
+
const raw = selectRawAssets(options.archiveFiles, resolved.themes, config.icons);
|
|
524
|
+
if (!raw.ok)
|
|
525
|
+
return raw;
|
|
526
|
+
for (const asset of raw.assets)
|
|
527
|
+
files.push({ path: rel(asset.path), content: asset.content });
|
|
528
|
+
}
|
|
529
|
+
const bitmapThemes = resolved.themes.filter((theme) => theme.kind === "bitmap" && theme.variant);
|
|
530
|
+
if (bitmapThemes.length > 0) {
|
|
531
|
+
const variants = bitmapThemes.flatMap((theme) => (theme.variant ? [theme.variant] : []));
|
|
532
|
+
const unique = [
|
|
533
|
+
...new Map(variants.map((variant) => [variant.resourceVariantId, variant])).values(),
|
|
534
|
+
];
|
|
535
|
+
if (options.archiveFiles === undefined) {
|
|
536
|
+
return {
|
|
537
|
+
ok: false,
|
|
538
|
+
errors: [
|
|
539
|
+
"bitmap themes require a downloaded free artifact; run `moeicons install` or provide a local archive fixture",
|
|
540
|
+
],
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
const selected = selectBitmapVariantAssets(options.archiveFiles, unique, config.icons);
|
|
544
|
+
if (!selected.ok)
|
|
545
|
+
return { ok: false, errors: [...selected.errors] };
|
|
546
|
+
for (const asset of selected.assets) {
|
|
547
|
+
files.push({ path: rel(asset.destRel), content: asset.bytes });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (target === "assets") {
|
|
551
|
+
const manifestAssets = files
|
|
552
|
+
.filter((file) => file.path.startsWith(`${outputDir.replace(/\/$/, "")}/assets/`) && !file.path.endsWith("/manifest.json"))
|
|
553
|
+
.map((file) => {
|
|
554
|
+
const path = file.path.slice(`${outputDir.replace(/\/$/, "")}/assets/`.length);
|
|
555
|
+
const bytes = typeof file.content === "string" ? Buffer.from(file.content, "utf8") : Buffer.from(file.content);
|
|
556
|
+
return { path, size: bytes.byteLength, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
557
|
+
})
|
|
558
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
559
|
+
files.push({ path: rel("assets/manifest.json"), content: `${JSON.stringify({ schemaVersion: 1, assets: manifestAssets }, null, 2)}\n` });
|
|
560
|
+
}
|
|
561
|
+
return { ok: true, files };
|
|
562
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CatalogStyleGroup, type IconCatalog } from "../catalog/catalog.js";
|
|
2
|
+
import type { MoeiconsConfigFile, MoeiconsThemeConfig } from "../project/config.js";
|
|
3
|
+
import { type ResourceVariant } from "../core/resource-variant.js";
|
|
4
|
+
import { toLibraryExportName, toProxyName } from "../core/icon-names.js";
|
|
5
|
+
export type ThemeKind = "svg" | "bitmap";
|
|
6
|
+
export interface ResolvedTheme {
|
|
7
|
+
readonly theme: string;
|
|
8
|
+
readonly entry: MoeiconsThemeConfig;
|
|
9
|
+
readonly kind: ThemeKind;
|
|
10
|
+
readonly group: CatalogStyleGroup;
|
|
11
|
+
readonly variant?: ResourceVariant;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveThemes(config: MoeiconsConfigFile, sourceCatalog?: IconCatalog): {
|
|
14
|
+
readonly ok: true;
|
|
15
|
+
readonly themes: ResolvedTheme[];
|
|
16
|
+
} | {
|
|
17
|
+
readonly ok: false;
|
|
18
|
+
readonly errors: string[];
|
|
19
|
+
};
|
|
20
|
+
export declare function bitmapWrapperImportName(theme: string, iconId: string): string;
|
|
21
|
+
export declare function svgInternalImportName(theme: string, styleGroup: string, iconId: string): string;
|
|
22
|
+
/** Relative import from icons/Foo.tsx → assets/... */
|
|
23
|
+
export declare function relativeAssetImportFromIcons(variant: ResourceVariant, iconId: string): string;
|
|
24
|
+
export declare function reactBitmapWrapperSource(iconId: string, wrapperName: string, variant: ResourceVariant, themeClassName: string | undefined): string;
|
|
25
|
+
export declare function vueBitmapWrapperSource(iconId: string, wrapperName: string, variant: ResourceVariant, themeClassName: string | undefined): string;
|
|
26
|
+
export { toLibraryExportName, toProxyName };
|