@featherk/icons 0.13.9 → 0.14.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/README.md +10 -0
- package/dist/fontawesome/build.cjs +4 -0
- package/dist/fontawesome/build.d.ts +3 -0
- package/dist/fontawesome/build.mjs +4 -0
- package/dist/fontawesome/chunks/generate-Bobf_ghm.cjs +288 -0
- package/dist/fontawesome/chunks/generate-Bp92GTVi.mjs +289 -0
- package/dist/fontawesome/cli.cjs +66 -0
- package/dist/fontawesome/cli.d.ts +6 -0
- package/dist/fontawesome/cli.mjs +66 -0
- package/dist/fontawesome/cli.test.d.ts +1 -0
- package/dist/fontawesome/fontawesome-service.d.ts +5 -0
- package/dist/fontawesome/fontawesome-service.test.d.ts +1 -0
- package/dist/fontawesome/generate.d.ts +18 -0
- package/dist/fontawesome/generate.test.d.ts +1 -0
- package/dist/fontawesome/index.cjs +20 -0
- package/dist/fontawesome/index.d.ts +3 -0
- package/dist/fontawesome/index.mjs +20 -0
- package/dist/fontawesome/normalize.d.ts +3 -0
- package/dist/fontawesome/normalize.test.d.ts +1 -0
- package/dist/fontawesome/types.d.ts +21 -0
- package/dist/fontawesome/vite.cjs +72 -0
- package/dist/fontawesome/vite.d.ts +6 -0
- package/dist/fontawesome/vite.mjs +72 -0
- package/dist/fontawesome/vite.test.d.ts +1 -0
- package/docs/ICONS.md +1 -1
- package/docs/fontawesome/CreateFontAwesomeManifest.md +109 -0
- package/docs/fontawesome/FontAwesome.md +252 -0
- package/docs/fontawesome/FontAwesomeCLI.md +64 -0
- package/docs/fontawesome/LICENSING.md +140 -0
- package/package.json +40 -4
- package/scripts/fontawesome.mjs +4 -0
package/README.md
CHANGED
|
@@ -66,6 +66,16 @@ variants, data URL usage, and related utilities) see [COMING-SOON.md](docs/COMIN
|
|
|
66
66
|
|
|
67
67
|
[View the icon gallery](https://github.com/NantHealth/featherk/blob/integration/packages/icons/docs/ICONS.md)
|
|
68
68
|
|
|
69
|
+
## Font Awesome Build Integration
|
|
70
|
+
|
|
71
|
+
Font Awesome services are separate from `getCustomIcon()` and the owned icon registry. The package supplies a Vite plugin, Node generator, and CLI to prepare selected icons from the consumer's installed Font Awesome packages; it includes no Free or paid Font Awesome artwork and grants no license rights.
|
|
72
|
+
|
|
73
|
+
Start with [Choose Font Awesome Icons and Create a Manifest](https://github.com/NantHealth/featherk/blob/integration/packages/icons/docs/fontawesome/CreateFontAwesomeManifest.md) for the UX/Design task of selecting and defining the exact icons your application will use.
|
|
74
|
+
|
|
75
|
+
Dev then follows [Font Awesome setup](https://github.com/NantHealth/featherk/blob/integration/packages/icons/docs/fontawesome/FontAwesome.md) for the default Vite integration. For Dev-owned CLI generation, early type preparation, or non-Vite applications, see [CLI and non-Vite setup](https://github.com/NantHealth/featherk/blob/integration/packages/icons/docs/fontawesome/FontAwesomeCLI.md).
|
|
76
|
+
|
|
77
|
+
For paid source packages, see [Pro licensing and setup](https://github.com/NantHealth/featherk/blob/integration/packages/icons/docs/fontawesome/LICENSING.md). Pro access is consumer-owned and optional; no paid artwork is distributed by this package.
|
|
78
|
+
|
|
69
79
|
## Adding icons
|
|
70
80
|
|
|
71
81
|
Icons in this package are curated from the validated source assets in
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const node_module = require("node:module");
|
|
3
|
+
const promises = require("node:fs/promises");
|
|
4
|
+
const node_path = require("node:path");
|
|
5
|
+
const node_crypto = require("node:crypto");
|
|
6
|
+
const prefixes = {
|
|
7
|
+
"classic:solid": "fas",
|
|
8
|
+
"classic:regular": "far",
|
|
9
|
+
"classic:light": "fal",
|
|
10
|
+
"classic:thin": "fat",
|
|
11
|
+
"classic:brands": "fab",
|
|
12
|
+
"sharp:solid": "fass",
|
|
13
|
+
"sharp:regular": "fasr",
|
|
14
|
+
"sharp:light": "fasl",
|
|
15
|
+
"sharp:thin": "fast",
|
|
16
|
+
"duotone:solid": "fad",
|
|
17
|
+
"duotone:regular": "fadr",
|
|
18
|
+
"duotone:light": "fadl",
|
|
19
|
+
"duotone:thin": "fadt",
|
|
20
|
+
"sharp-duotone:solid": "fasds",
|
|
21
|
+
"sharp-duotone:regular": "fasdr",
|
|
22
|
+
"sharp-duotone:light": "fasdl",
|
|
23
|
+
"sharp-duotone:thin": "fasdt"
|
|
24
|
+
};
|
|
25
|
+
function expectedPrefix(selection) {
|
|
26
|
+
const prefix = prefixes[`${selection.family}:${selection.style}`];
|
|
27
|
+
if (!prefix) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Unsupported Font Awesome family/style for "${selection.key}"`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return prefix;
|
|
33
|
+
}
|
|
34
|
+
function normalizeIcon(selection, definition) {
|
|
35
|
+
if (!definition || typeof definition !== "object") {
|
|
36
|
+
throw new Error(`Missing Font Awesome definition for "${selection.key}"`);
|
|
37
|
+
}
|
|
38
|
+
const source = definition;
|
|
39
|
+
if (source.iconName !== selection.name || source.prefix !== expectedPrefix(selection)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Font Awesome name/family/style mismatch for "${selection.key}"`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(source.icon) || source.icon.length !== 5) {
|
|
45
|
+
throw new Error(`Invalid Font Awesome geometry for "${selection.key}"`);
|
|
46
|
+
}
|
|
47
|
+
const [width, height, , , pathData] = source.icon;
|
|
48
|
+
if (typeof width !== "number" || !Number.isFinite(width) || width <= 0 || typeof height !== "number" || !Number.isFinite(height) || height <= 0) {
|
|
49
|
+
throw new Error(`Invalid Font Awesome dimensions for "${selection.key}"`);
|
|
50
|
+
}
|
|
51
|
+
const paths = Array.isArray(pathData) ? pathData : [pathData];
|
|
52
|
+
const duotone = selection.family === "duotone" || selection.family === "sharp-duotone";
|
|
53
|
+
if (paths.length !== (duotone ? 2 : 1)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Unsupported Font Awesome path layers for "${selection.key}"`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
for (const path of paths) {
|
|
59
|
+
if (typeof path !== "string" || !/^\s*[Mm]/.test(path) || !/^[MmZzLlHhVvCcSsQqTtAa0-9eE+.,\s-]+$/.test(path)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Unsafe or invalid Font Awesome path for "${selection.key}"`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const content = duotone ? `<path d="${paths[0]}" fill="var(--fa-secondary-color, currentColor)" opacity="var(--fa-secondary-opacity, 0.4)"/><path d="${paths[1]}" fill="var(--fa-primary-color, currentColor)" opacity="var(--fa-primary-opacity, 1)"/>` : `<path d="${paths[0]}" fill="currentColor"/>`;
|
|
66
|
+
return { name: selection.key, content, viewBox: `0 0 ${width} ${height}` };
|
|
67
|
+
}
|
|
68
|
+
function validateSelection(selection) {
|
|
69
|
+
if (!selection || typeof selection !== "object" || typeof selection.key !== "string" || !selection.key.trim()) {
|
|
70
|
+
throw new Error("Font Awesome selections require a non-empty key");
|
|
71
|
+
}
|
|
72
|
+
if (typeof selection.name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(selection.name)) {
|
|
73
|
+
throw new Error(`Invalid Font Awesome name for "${selection.key}"`);
|
|
74
|
+
}
|
|
75
|
+
if (typeof selection.version !== "string" || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
|
|
76
|
+
selection.version
|
|
77
|
+
)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Font Awesome "${selection.key}" requires an exact version`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (typeof selection.package !== "string" || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(
|
|
83
|
+
selection.package
|
|
84
|
+
)) {
|
|
85
|
+
throw new Error(`Invalid Font Awesome package for "${selection.key}"`);
|
|
86
|
+
}
|
|
87
|
+
expectedPrefix(selection);
|
|
88
|
+
const exportName = selection.exportName ?? `fa${selection.name.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join("")}`;
|
|
89
|
+
if (typeof exportName !== "string" || !/^fa[A-Z0-9][A-Za-z0-9]*$/.test(exportName)) {
|
|
90
|
+
throw new Error(`Invalid Font Awesome export for "${selection.key}"`);
|
|
91
|
+
}
|
|
92
|
+
return exportName;
|
|
93
|
+
}
|
|
94
|
+
function serialize(value) {
|
|
95
|
+
return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
96
|
+
}
|
|
97
|
+
async function validateOutput(root, file, declaration = false) {
|
|
98
|
+
const target = node_path.resolve(root, file);
|
|
99
|
+
const rootPath = await promises.realpath(root);
|
|
100
|
+
const relativeTarget = node_path.relative(rootPath, target);
|
|
101
|
+
if (node_path.isAbsolute(relativeTarget) || relativeTarget === ".." || relativeTarget.startsWith(`..${node_path.sep}`) || relativeTarget.split(node_path.sep).includes("node_modules")) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"Font Awesome output must be inside the consumer project, outside node_modules"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if ((declaration ? !target.endsWith(".d.ts") : ![".js", ".mjs"].includes(node_path.extname(target))) || node_path.basename(target).startsWith(".")) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
declaration ? "Font Awesome virtual types output must be a named .d.ts file" : "Font Awesome output must be a named .js or .mjs module"
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
let current = node_path.dirname(target);
|
|
112
|
+
while (true) {
|
|
113
|
+
try {
|
|
114
|
+
const actual = await promises.realpath(current);
|
|
115
|
+
const boundary = node_path.relative(rootPath, actual);
|
|
116
|
+
if (node_path.isAbsolute(boundary) || boundary === ".." || boundary.startsWith(`..${node_path.sep}`)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
"Font Awesome output cannot escape the consumer project through a symlink"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const metadata = JSON.parse(
|
|
122
|
+
await promises.readFile(node_path.join(current, "package.json"), "utf8")
|
|
123
|
+
);
|
|
124
|
+
if (metadata.name === "@featherk/icons") {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"Font Awesome output must not target the @featherk/icons package"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error.code !== "ENOENT") throw error;
|
|
131
|
+
}
|
|
132
|
+
if (current === rootPath) break;
|
|
133
|
+
const parent = node_path.dirname(current);
|
|
134
|
+
if (parent === current) throw new Error("Invalid Font Awesome output root");
|
|
135
|
+
current = parent;
|
|
136
|
+
}
|
|
137
|
+
for (const output of [
|
|
138
|
+
target,
|
|
139
|
+
target.replace(/\.(m?)js$/, ".d.$1ts"),
|
|
140
|
+
`${target}.licenses.txt`
|
|
141
|
+
]) {
|
|
142
|
+
try {
|
|
143
|
+
const actual = await promises.realpath(output);
|
|
144
|
+
if (actual !== output)
|
|
145
|
+
throw new Error("Font Awesome output files must not be symlinks");
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error.code !== "ENOENT") throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return target;
|
|
151
|
+
}
|
|
152
|
+
async function writeOutput(target, result) {
|
|
153
|
+
await promises.mkdir(node_path.dirname(target), { recursive: true });
|
|
154
|
+
const outputs = [
|
|
155
|
+
[target, result.moduleCode],
|
|
156
|
+
[target.replace(/\.(m?)js$/, ".d.$1ts"), result.declarationCode],
|
|
157
|
+
[`${target}.licenses.txt`, result.notices]
|
|
158
|
+
];
|
|
159
|
+
for (const [file, content] of outputs) {
|
|
160
|
+
await writeAtomically(file, content);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function writeAtomically(file, content) {
|
|
164
|
+
await promises.mkdir(node_path.dirname(file), { recursive: true });
|
|
165
|
+
const temporary = `${file}.${node_crypto.randomUUID()}.tmp`;
|
|
166
|
+
try {
|
|
167
|
+
await promises.writeFile(temporary, content, { flag: "wx" });
|
|
168
|
+
await promises.rename(temporary, file);
|
|
169
|
+
} finally {
|
|
170
|
+
await promises.unlink(temporary).catch((error) => {
|
|
171
|
+
if (error.code !== "ENOENT") throw error;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function generateFaIcons(options) {
|
|
176
|
+
const root = await promises.realpath(node_path.resolve(options.root ?? process.cwd()));
|
|
177
|
+
if (!Array.isArray(options.icons))
|
|
178
|
+
throw new Error("Font Awesome icons must be an array of selections");
|
|
179
|
+
const selections = options.icons.map((selection) => {
|
|
180
|
+
if (!selection || typeof selection !== "object") {
|
|
181
|
+
throw new Error("Font Awesome selections must be objects");
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
key: selection.key,
|
|
185
|
+
name: selection.name,
|
|
186
|
+
version: selection.version,
|
|
187
|
+
family: selection.family,
|
|
188
|
+
style: selection.style,
|
|
189
|
+
package: selection.package,
|
|
190
|
+
...selection.exportName === void 0 ? {} : { exportName: selection.exportName }
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
const keys = /* @__PURE__ */ new Set();
|
|
194
|
+
const exports$1 = selections.map((selection) => {
|
|
195
|
+
const exportName = validateSelection(selection);
|
|
196
|
+
if (keys.has(selection.key))
|
|
197
|
+
throw new Error(`Duplicate Font Awesome key "${selection.key}"`);
|
|
198
|
+
keys.add(selection.key);
|
|
199
|
+
return exportName;
|
|
200
|
+
});
|
|
201
|
+
const target = options.outFile === void 0 ? void 0 : await validateOutput(root, options.outFile);
|
|
202
|
+
const virtualTypesTarget = options.virtualTypesFile === void 0 ? void 0 : await validateOutput(root, options.virtualTypesFile, true);
|
|
203
|
+
if (target && virtualTypesTarget === target.replace(/\.(m?)js$/, ".d.$1ts")) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"Virtual types output must not overwrite the generated module's declarations"
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const sourceRoot = options.sourceRoot === void 0 ? root : await promises.realpath(node_path.resolve(root, options.sourceRoot));
|
|
209
|
+
const requireSource = node_module.createRequire(node_path.join(sourceRoot, "package.json"));
|
|
210
|
+
const icons = /* @__PURE__ */ Object.create(null);
|
|
211
|
+
const provenance = [];
|
|
212
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
213
|
+
const notices = /* @__PURE__ */ new Map();
|
|
214
|
+
for (const [index, selection] of selections.entries()) {
|
|
215
|
+
const exportName = exports$1[index];
|
|
216
|
+
let metadataFile;
|
|
217
|
+
let definitionFile;
|
|
218
|
+
try {
|
|
219
|
+
metadataFile = requireSource.resolve(`${selection.package}/package.json`);
|
|
220
|
+
definitionFile = requireSource.resolve(
|
|
221
|
+
`${selection.package}/${exportName}`
|
|
222
|
+
);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Cannot resolve installed Font Awesome source "${selection.package}/${exportName}" for "${selection.key}". Install its pinned package and expose package.json and individual icon modules.`,
|
|
226
|
+
{ cause: error }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const metadata = JSON.parse(await promises.readFile(metadataFile, "utf8"));
|
|
230
|
+
if (metadata.version !== selection.version) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Font Awesome version mismatch for "${selection.key}": requested ${selection.version}, installed ${metadata.version}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (typeof metadata.name !== "string" || !metadata.name) {
|
|
236
|
+
throw new Error(`Missing source package name for "${selection.key}"`);
|
|
237
|
+
}
|
|
238
|
+
const licenseFile = node_path.join(node_path.dirname(metadataFile), "LICENSE.txt");
|
|
239
|
+
if (!notices.has(metadataFile)) {
|
|
240
|
+
const text = await promises.readFile(licenseFile, "utf8");
|
|
241
|
+
if (!text.trim())
|
|
242
|
+
throw new Error(`Missing license notice for "${selection.package}"`);
|
|
243
|
+
notices.set(
|
|
244
|
+
metadataFile,
|
|
245
|
+
`${metadata.name}@${metadata.version}
|
|
246
|
+
${text.trim()}
|
|
247
|
+
`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
dependencies.add(metadataFile);
|
|
251
|
+
dependencies.add(licenseFile);
|
|
252
|
+
dependencies.add(definitionFile);
|
|
253
|
+
delete requireSource.cache[definitionFile];
|
|
254
|
+
const source = requireSource(definitionFile);
|
|
255
|
+
if (!Object.hasOwn(source, exportName)) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Missing Font Awesome export "${exportName}" for "${selection.key}"`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
icons[selection.key] = normalizeIcon(selection, source[exportName]);
|
|
261
|
+
provenance.push({ ...selection, exportName, sourcePackage: metadata.name });
|
|
262
|
+
}
|
|
263
|
+
const result = {
|
|
264
|
+
moduleCode: `import { createFontAwesomeService } from "@featherk/icons/fontawesome";
|
|
265
|
+
const selections = JSON.parse(${serialize(serialize(icons))});
|
|
266
|
+
export const getFaIcon = createFontAwesomeService(selections).getFaIcon;
|
|
267
|
+
export const provenance = ${serialize(provenance)};
|
|
268
|
+
`,
|
|
269
|
+
declarationCode: `import type { FontAwesomeIcon, FontAwesomeSelection } from "@featherk/icons/fontawesome";
|
|
270
|
+
export type FontAwesomeKey = ${selections.length ? selections.map((selection) => serialize(selection.key)).join(" | ") : "never"};
|
|
271
|
+
export declare function getFaIcon(key: FontAwesomeKey): FontAwesomeIcon;
|
|
272
|
+
export declare const provenance: readonly (FontAwesomeSelection & { sourcePackage: string; exportName: string })[];
|
|
273
|
+
`,
|
|
274
|
+
virtualDeclarationCode: "",
|
|
275
|
+
notices: [...notices.values()].join("\n"),
|
|
276
|
+
provenance,
|
|
277
|
+
dependencies: [...dependencies]
|
|
278
|
+
};
|
|
279
|
+
result.virtualDeclarationCode = `declare module "virtual:featherk/fontawesome" {
|
|
280
|
+
${result.declarationCode.replaceAll("export declare ", "export ")}
|
|
281
|
+
}
|
|
282
|
+
`;
|
|
283
|
+
if (target) await writeOutput(target, result);
|
|
284
|
+
if (virtualTypesTarget)
|
|
285
|
+
await writeAtomically(virtualTypesTarget, result.virtualDeclarationCode);
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
exports.generateFaIcons = generateFaIcons;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { realpath, readFile, mkdir, writeFile, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { resolve, join, dirname, relative, isAbsolute, sep, extname, basename } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
const prefixes = {
|
|
6
|
+
"classic:solid": "fas",
|
|
7
|
+
"classic:regular": "far",
|
|
8
|
+
"classic:light": "fal",
|
|
9
|
+
"classic:thin": "fat",
|
|
10
|
+
"classic:brands": "fab",
|
|
11
|
+
"sharp:solid": "fass",
|
|
12
|
+
"sharp:regular": "fasr",
|
|
13
|
+
"sharp:light": "fasl",
|
|
14
|
+
"sharp:thin": "fast",
|
|
15
|
+
"duotone:solid": "fad",
|
|
16
|
+
"duotone:regular": "fadr",
|
|
17
|
+
"duotone:light": "fadl",
|
|
18
|
+
"duotone:thin": "fadt",
|
|
19
|
+
"sharp-duotone:solid": "fasds",
|
|
20
|
+
"sharp-duotone:regular": "fasdr",
|
|
21
|
+
"sharp-duotone:light": "fasdl",
|
|
22
|
+
"sharp-duotone:thin": "fasdt"
|
|
23
|
+
};
|
|
24
|
+
function expectedPrefix(selection) {
|
|
25
|
+
const prefix = prefixes[`${selection.family}:${selection.style}`];
|
|
26
|
+
if (!prefix) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Unsupported Font Awesome family/style for "${selection.key}"`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return prefix;
|
|
32
|
+
}
|
|
33
|
+
function normalizeIcon(selection, definition) {
|
|
34
|
+
if (!definition || typeof definition !== "object") {
|
|
35
|
+
throw new Error(`Missing Font Awesome definition for "${selection.key}"`);
|
|
36
|
+
}
|
|
37
|
+
const source = definition;
|
|
38
|
+
if (source.iconName !== selection.name || source.prefix !== expectedPrefix(selection)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Font Awesome name/family/style mismatch for "${selection.key}"`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (!Array.isArray(source.icon) || source.icon.length !== 5) {
|
|
44
|
+
throw new Error(`Invalid Font Awesome geometry for "${selection.key}"`);
|
|
45
|
+
}
|
|
46
|
+
const [width, height, , , pathData] = source.icon;
|
|
47
|
+
if (typeof width !== "number" || !Number.isFinite(width) || width <= 0 || typeof height !== "number" || !Number.isFinite(height) || height <= 0) {
|
|
48
|
+
throw new Error(`Invalid Font Awesome dimensions for "${selection.key}"`);
|
|
49
|
+
}
|
|
50
|
+
const paths = Array.isArray(pathData) ? pathData : [pathData];
|
|
51
|
+
const duotone = selection.family === "duotone" || selection.family === "sharp-duotone";
|
|
52
|
+
if (paths.length !== (duotone ? 2 : 1)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Unsupported Font Awesome path layers for "${selection.key}"`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
for (const path of paths) {
|
|
58
|
+
if (typeof path !== "string" || !/^\s*[Mm]/.test(path) || !/^[MmZzLlHhVvCcSsQqTtAa0-9eE+.,\s-]+$/.test(path)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Unsafe or invalid Font Awesome path for "${selection.key}"`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const content = duotone ? `<path d="${paths[0]}" fill="var(--fa-secondary-color, currentColor)" opacity="var(--fa-secondary-opacity, 0.4)"/><path d="${paths[1]}" fill="var(--fa-primary-color, currentColor)" opacity="var(--fa-primary-opacity, 1)"/>` : `<path d="${paths[0]}" fill="currentColor"/>`;
|
|
65
|
+
return { name: selection.key, content, viewBox: `0 0 ${width} ${height}` };
|
|
66
|
+
}
|
|
67
|
+
function validateSelection(selection) {
|
|
68
|
+
if (!selection || typeof selection !== "object" || typeof selection.key !== "string" || !selection.key.trim()) {
|
|
69
|
+
throw new Error("Font Awesome selections require a non-empty key");
|
|
70
|
+
}
|
|
71
|
+
if (typeof selection.name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(selection.name)) {
|
|
72
|
+
throw new Error(`Invalid Font Awesome name for "${selection.key}"`);
|
|
73
|
+
}
|
|
74
|
+
if (typeof selection.version !== "string" || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
|
|
75
|
+
selection.version
|
|
76
|
+
)) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Font Awesome "${selection.key}" requires an exact version`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (typeof selection.package !== "string" || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(
|
|
82
|
+
selection.package
|
|
83
|
+
)) {
|
|
84
|
+
throw new Error(`Invalid Font Awesome package for "${selection.key}"`);
|
|
85
|
+
}
|
|
86
|
+
expectedPrefix(selection);
|
|
87
|
+
const exportName = selection.exportName ?? `fa${selection.name.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join("")}`;
|
|
88
|
+
if (typeof exportName !== "string" || !/^fa[A-Z0-9][A-Za-z0-9]*$/.test(exportName)) {
|
|
89
|
+
throw new Error(`Invalid Font Awesome export for "${selection.key}"`);
|
|
90
|
+
}
|
|
91
|
+
return exportName;
|
|
92
|
+
}
|
|
93
|
+
function serialize(value) {
|
|
94
|
+
return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
95
|
+
}
|
|
96
|
+
async function validateOutput(root, file, declaration = false) {
|
|
97
|
+
const target = resolve(root, file);
|
|
98
|
+
const rootPath = await realpath(root);
|
|
99
|
+
const relativeTarget = relative(rootPath, target);
|
|
100
|
+
if (isAbsolute(relativeTarget) || relativeTarget === ".." || relativeTarget.startsWith(`..${sep}`) || relativeTarget.split(sep).includes("node_modules")) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"Font Awesome output must be inside the consumer project, outside node_modules"
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if ((declaration ? !target.endsWith(".d.ts") : ![".js", ".mjs"].includes(extname(target))) || basename(target).startsWith(".")) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
declaration ? "Font Awesome virtual types output must be a named .d.ts file" : "Font Awesome output must be a named .js or .mjs module"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
let current = dirname(target);
|
|
111
|
+
while (true) {
|
|
112
|
+
try {
|
|
113
|
+
const actual = await realpath(current);
|
|
114
|
+
const boundary = relative(rootPath, actual);
|
|
115
|
+
if (isAbsolute(boundary) || boundary === ".." || boundary.startsWith(`..${sep}`)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"Font Awesome output cannot escape the consumer project through a symlink"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const metadata = JSON.parse(
|
|
121
|
+
await readFile(join(current, "package.json"), "utf8")
|
|
122
|
+
);
|
|
123
|
+
if (metadata.name === "@featherk/icons") {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"Font Awesome output must not target the @featherk/icons package"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error.code !== "ENOENT") throw error;
|
|
130
|
+
}
|
|
131
|
+
if (current === rootPath) break;
|
|
132
|
+
const parent = dirname(current);
|
|
133
|
+
if (parent === current) throw new Error("Invalid Font Awesome output root");
|
|
134
|
+
current = parent;
|
|
135
|
+
}
|
|
136
|
+
for (const output of [
|
|
137
|
+
target,
|
|
138
|
+
target.replace(/\.(m?)js$/, ".d.$1ts"),
|
|
139
|
+
`${target}.licenses.txt`
|
|
140
|
+
]) {
|
|
141
|
+
try {
|
|
142
|
+
const actual = await realpath(output);
|
|
143
|
+
if (actual !== output)
|
|
144
|
+
throw new Error("Font Awesome output files must not be symlinks");
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error.code !== "ENOENT") throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return target;
|
|
150
|
+
}
|
|
151
|
+
async function writeOutput(target, result) {
|
|
152
|
+
await mkdir(dirname(target), { recursive: true });
|
|
153
|
+
const outputs = [
|
|
154
|
+
[target, result.moduleCode],
|
|
155
|
+
[target.replace(/\.(m?)js$/, ".d.$1ts"), result.declarationCode],
|
|
156
|
+
[`${target}.licenses.txt`, result.notices]
|
|
157
|
+
];
|
|
158
|
+
for (const [file, content] of outputs) {
|
|
159
|
+
await writeAtomically(file, content);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function writeAtomically(file, content) {
|
|
163
|
+
await mkdir(dirname(file), { recursive: true });
|
|
164
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
165
|
+
try {
|
|
166
|
+
await writeFile(temporary, content, { flag: "wx" });
|
|
167
|
+
await rename(temporary, file);
|
|
168
|
+
} finally {
|
|
169
|
+
await unlink(temporary).catch((error) => {
|
|
170
|
+
if (error.code !== "ENOENT") throw error;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function generateFaIcons(options) {
|
|
175
|
+
const root = await realpath(resolve(options.root ?? process.cwd()));
|
|
176
|
+
if (!Array.isArray(options.icons))
|
|
177
|
+
throw new Error("Font Awesome icons must be an array of selections");
|
|
178
|
+
const selections = options.icons.map((selection) => {
|
|
179
|
+
if (!selection || typeof selection !== "object") {
|
|
180
|
+
throw new Error("Font Awesome selections must be objects");
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
key: selection.key,
|
|
184
|
+
name: selection.name,
|
|
185
|
+
version: selection.version,
|
|
186
|
+
family: selection.family,
|
|
187
|
+
style: selection.style,
|
|
188
|
+
package: selection.package,
|
|
189
|
+
...selection.exportName === void 0 ? {} : { exportName: selection.exportName }
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
const keys = /* @__PURE__ */ new Set();
|
|
193
|
+
const exports$1 = selections.map((selection) => {
|
|
194
|
+
const exportName = validateSelection(selection);
|
|
195
|
+
if (keys.has(selection.key))
|
|
196
|
+
throw new Error(`Duplicate Font Awesome key "${selection.key}"`);
|
|
197
|
+
keys.add(selection.key);
|
|
198
|
+
return exportName;
|
|
199
|
+
});
|
|
200
|
+
const target = options.outFile === void 0 ? void 0 : await validateOutput(root, options.outFile);
|
|
201
|
+
const virtualTypesTarget = options.virtualTypesFile === void 0 ? void 0 : await validateOutput(root, options.virtualTypesFile, true);
|
|
202
|
+
if (target && virtualTypesTarget === target.replace(/\.(m?)js$/, ".d.$1ts")) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
"Virtual types output must not overwrite the generated module's declarations"
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const sourceRoot = options.sourceRoot === void 0 ? root : await realpath(resolve(root, options.sourceRoot));
|
|
208
|
+
const requireSource = createRequire(join(sourceRoot, "package.json"));
|
|
209
|
+
const icons = /* @__PURE__ */ Object.create(null);
|
|
210
|
+
const provenance = [];
|
|
211
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
212
|
+
const notices = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const [index, selection] of selections.entries()) {
|
|
214
|
+
const exportName = exports$1[index];
|
|
215
|
+
let metadataFile;
|
|
216
|
+
let definitionFile;
|
|
217
|
+
try {
|
|
218
|
+
metadataFile = requireSource.resolve(`${selection.package}/package.json`);
|
|
219
|
+
definitionFile = requireSource.resolve(
|
|
220
|
+
`${selection.package}/${exportName}`
|
|
221
|
+
);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Cannot resolve installed Font Awesome source "${selection.package}/${exportName}" for "${selection.key}". Install its pinned package and expose package.json and individual icon modules.`,
|
|
225
|
+
{ cause: error }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const metadata = JSON.parse(await readFile(metadataFile, "utf8"));
|
|
229
|
+
if (metadata.version !== selection.version) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Font Awesome version mismatch for "${selection.key}": requested ${selection.version}, installed ${metadata.version}`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (typeof metadata.name !== "string" || !metadata.name) {
|
|
235
|
+
throw new Error(`Missing source package name for "${selection.key}"`);
|
|
236
|
+
}
|
|
237
|
+
const licenseFile = join(dirname(metadataFile), "LICENSE.txt");
|
|
238
|
+
if (!notices.has(metadataFile)) {
|
|
239
|
+
const text = await readFile(licenseFile, "utf8");
|
|
240
|
+
if (!text.trim())
|
|
241
|
+
throw new Error(`Missing license notice for "${selection.package}"`);
|
|
242
|
+
notices.set(
|
|
243
|
+
metadataFile,
|
|
244
|
+
`${metadata.name}@${metadata.version}
|
|
245
|
+
${text.trim()}
|
|
246
|
+
`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
dependencies.add(metadataFile);
|
|
250
|
+
dependencies.add(licenseFile);
|
|
251
|
+
dependencies.add(definitionFile);
|
|
252
|
+
delete requireSource.cache[definitionFile];
|
|
253
|
+
const source = requireSource(definitionFile);
|
|
254
|
+
if (!Object.hasOwn(source, exportName)) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Missing Font Awesome export "${exportName}" for "${selection.key}"`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
icons[selection.key] = normalizeIcon(selection, source[exportName]);
|
|
260
|
+
provenance.push({ ...selection, exportName, sourcePackage: metadata.name });
|
|
261
|
+
}
|
|
262
|
+
const result = {
|
|
263
|
+
moduleCode: `import { createFontAwesomeService } from "@featherk/icons/fontawesome";
|
|
264
|
+
const selections = JSON.parse(${serialize(serialize(icons))});
|
|
265
|
+
export const getFaIcon = createFontAwesomeService(selections).getFaIcon;
|
|
266
|
+
export const provenance = ${serialize(provenance)};
|
|
267
|
+
`,
|
|
268
|
+
declarationCode: `import type { FontAwesomeIcon, FontAwesomeSelection } from "@featherk/icons/fontawesome";
|
|
269
|
+
export type FontAwesomeKey = ${selections.length ? selections.map((selection) => serialize(selection.key)).join(" | ") : "never"};
|
|
270
|
+
export declare function getFaIcon(key: FontAwesomeKey): FontAwesomeIcon;
|
|
271
|
+
export declare const provenance: readonly (FontAwesomeSelection & { sourcePackage: string; exportName: string })[];
|
|
272
|
+
`,
|
|
273
|
+
virtualDeclarationCode: "",
|
|
274
|
+
notices: [...notices.values()].join("\n"),
|
|
275
|
+
provenance,
|
|
276
|
+
dependencies: [...dependencies]
|
|
277
|
+
};
|
|
278
|
+
result.virtualDeclarationCode = `declare module "virtual:featherk/fontawesome" {
|
|
279
|
+
${result.declarationCode.replaceAll("export declare ", "export ")}
|
|
280
|
+
}
|
|
281
|
+
`;
|
|
282
|
+
if (target) await writeOutput(target, result);
|
|
283
|
+
if (virtualTypesTarget)
|
|
284
|
+
await writeAtomically(virtualTypesTarget, result.virtualDeclarationCode);
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
export {
|
|
288
|
+
generateFaIcons as g
|
|
289
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const promises = require("node:fs/promises");
|
|
4
|
+
const node_path = require("node:path");
|
|
5
|
+
const node_util = require("node:util");
|
|
6
|
+
const generate = require("./chunks/generate-Bobf_ghm.cjs");
|
|
7
|
+
const usage = `Usage: featherk-fontawesome --manifest <icons.json> [--root <project>] [--source-root <installed-sources>] [--out <icons.mjs>] [--virtual-types <types.d.ts>]
|
|
8
|
+
|
|
9
|
+
The JSON manifest must contain { "icons": [...] } with exact installed selections.
|
|
10
|
+
Provide --out, --virtual-types, or both. Paths are relative to the project root.
|
|
11
|
+
This command does not download assets or authenticate; install licensed packages first.
|
|
12
|
+
Generated assets belong to the consuming project, never the @featherk/icons package.`;
|
|
13
|
+
async function runFontAwesomeCli(args, io = {}) {
|
|
14
|
+
const stdout = io.stdout ?? console.log;
|
|
15
|
+
const stderr = io.stderr ?? console.error;
|
|
16
|
+
try {
|
|
17
|
+
const { values } = node_util.parseArgs({
|
|
18
|
+
args: [...args],
|
|
19
|
+
options: {
|
|
20
|
+
manifest: { type: "string" },
|
|
21
|
+
root: { type: "string" },
|
|
22
|
+
"source-root": { type: "string" },
|
|
23
|
+
out: { type: "string" },
|
|
24
|
+
"virtual-types": { type: "string" },
|
|
25
|
+
help: { type: "boolean", short: "h" }
|
|
26
|
+
},
|
|
27
|
+
strict: true,
|
|
28
|
+
allowPositionals: false
|
|
29
|
+
});
|
|
30
|
+
if (values.help) {
|
|
31
|
+
stdout(usage);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
if (!values.manifest?.trim() || !values.out?.trim() && !values["virtual-types"]?.trim()) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Provide --manifest and at least one of --out or --virtual-types. Use --help for usage."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const root = node_path.resolve(io.cwd ?? process.cwd(), values.root ?? ".");
|
|
40
|
+
const manifest = JSON.parse(
|
|
41
|
+
await promises.readFile(node_path.resolve(root, values.manifest), "utf8")
|
|
42
|
+
);
|
|
43
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest) || !Array.isArray(manifest.icons)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'Font Awesome manifest must be a JSON object with an "icons" array'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const result = await generate.generateFaIcons({
|
|
49
|
+
root,
|
|
50
|
+
sourceRoot: values["source-root"],
|
|
51
|
+
icons: manifest.icons,
|
|
52
|
+
outFile: values.out,
|
|
53
|
+
virtualTypesFile: values["virtual-types"]
|
|
54
|
+
});
|
|
55
|
+
stdout(
|
|
56
|
+
`Prepared ${result.provenance.length} selected Font Awesome icon(s). Preserve generated license notices in application distribution.`
|
|
57
|
+
);
|
|
58
|
+
return 0;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
stderr(
|
|
61
|
+
`Font Awesome generation failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
62
|
+
);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.runFontAwesomeCli = runFontAwesomeCli;
|