@ethlete/components 0.1.0-next.14 → 0.1.0-next.16

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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "generators": {
3
+ "icons": {
4
+ "factory": "./icons/generator",
5
+ "schema": "./icons/schema.json",
6
+ "description": "Generate etIcon IconDefinitions and typed names from an installed SVG icon source (auto-detects Font Awesome)"
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,183 @@
1
+ import { formatFiles, logger, workspaceRoot } from '@nx/devkit';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ //#endregion
5
+ //#region Constants
6
+ // Known SVG sources, tried in order during auto-detection. All use a `svgs/<variant>/<name>.svg` layout.
7
+ const KNOWN_SOURCES = ['@fortawesome/fontawesome-pro', '@fortawesome/fontawesome-free'];
8
+ const DEFAULT_CONFIG_PATH = 'src/icons.json';
9
+ const DEFAULT_OUTPUT_PATH = 'src/generated/et-icons.ts';
10
+ const DEFAULT_VARIANT = 'solid';
11
+ //#endregion
12
+ export default async function generate(tree, schema) {
13
+ logger.log('\nšŸŽØ Starting Ethlete icon generator...\n');
14
+ const configPath = schema.configPath || DEFAULT_CONFIG_PATH;
15
+ const outputPath = schema.outputPath || DEFAULT_OUTPUT_PATH;
16
+ const typesOutputPath = schema.typesOutputPath || join(dirname(outputPath), 'et-icon-registry.d.ts');
17
+ // Step 1: Read the icons config.
18
+ if (!tree.exists(configPath)) {
19
+ logger.error(`āŒ Icons config not found at: ${configPath}`);
20
+ logger.log(`\nCreate one, or point to it with --configPath. Example ${configPath}:`);
21
+ logger.log(` { "source": "auto", "defaultVariant": "solid", "icons": ["plus", { "name": "shield", "variants": ["light", "solid"] }] }\n`);
22
+ return;
23
+ }
24
+ let config;
25
+ try {
26
+ config = JSON.parse(tree.read(configPath, 'utf-8') || '');
27
+ }
28
+ catch (error) {
29
+ logger.error(`āŒ Failed to parse ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
30
+ return;
31
+ }
32
+ if (!Array.isArray(config.icons) || config.icons.length === 0) {
33
+ logger.error(`āŒ ${configPath} must contain a non-empty "icons" array.`);
34
+ return;
35
+ }
36
+ const defaultVariant = config.defaultVariant || DEFAULT_VARIANT;
37
+ // Step 2: Resolve the SVG source package.
38
+ const requestedSource = schema.source && schema.source !== 'auto' ? schema.source : config.source;
39
+ const source = resolveSource(requestedSource);
40
+ if (!source) {
41
+ logger.error('āŒ Could not find an icon source. Install @fortawesome/fontawesome-pro (or -free), or set "source".');
42
+ return;
43
+ }
44
+ logger.log(`šŸ“¦ Using icon source: ${source.package}`);
45
+ // Step 3: Read + transform each icon's SVG.
46
+ const requested = normalizeIcons(config.icons, defaultVariant);
47
+ const resolved = [];
48
+ const missing = [];
49
+ for (const icon of requested) {
50
+ const svgPath = join(source.svgsDir, icon.variant, `${icon.name}.svg`);
51
+ if (!existsSync(svgPath)) {
52
+ missing.push(`${icon.name} (${icon.variant})`);
53
+ continue;
54
+ }
55
+ resolved.push({ ...icon, data: toIconData(readFileSync(svgPath, 'utf-8')) });
56
+ }
57
+ if (missing.length) {
58
+ logger.warn(`āš ļø ${missing.length} icon(s) not found in ${source.package} and skipped:\n - ${missing.join('\n - ')}`);
59
+ }
60
+ if (resolved.length === 0) {
61
+ logger.error('āŒ No icons could be resolved. Nothing was written.');
62
+ return;
63
+ }
64
+ // Step 4: Write the IconDefinition constants.
65
+ tree.write(outputPath, generateIconsFile(resolved, schema, source.package));
66
+ logger.log(`āœ… Generated ${resolved.length} icon(s) at: ${outputPath}`);
67
+ // Step 5: Write the type augmentation so `etIcon`/`variant` are checked against these names.
68
+ tree.write(typesOutputPath, generateTypesFile(resolved, schema));
69
+ logger.log(`āœ… Generated icon name types at: ${typesOutputPath}`);
70
+ if (!schema.skipFormat) {
71
+ await formatFiles(tree);
72
+ }
73
+ logger.log('\nāœ… Icon generation completed successfully!\n');
74
+ }
75
+ //#region Helpers
76
+ function resolveSource(requested) {
77
+ const candidates = requested ? [requested] : KNOWN_SOURCES;
78
+ for (const pkg of candidates) {
79
+ const svgsDir = join(workspaceRoot, 'node_modules', pkg, 'svgs');
80
+ if (existsSync(svgsDir)) {
81
+ return { package: pkg, svgsDir };
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ function normalizeIcons(entries, defaultVariant) {
87
+ const seen = new Set();
88
+ const result = [];
89
+ const add = (name, variant) => {
90
+ const key = `${name}::${variant}`;
91
+ if (!seen.has(key)) {
92
+ seen.add(key);
93
+ result.push({ name, variant });
94
+ }
95
+ };
96
+ for (const entry of entries) {
97
+ if (typeof entry === 'string') {
98
+ add(entry, defaultVariant);
99
+ continue;
100
+ }
101
+ const variants = entry.variants ?? [entry.variant ?? defaultVariant];
102
+ for (const variant of variants) {
103
+ add(entry.name, variant);
104
+ }
105
+ }
106
+ // Stable ordering keeps regeneration diffs minimal.
107
+ return result.sort((a, b) => `${a.name}::${a.variant}`.localeCompare(`${b.name}::${b.variant}`));
108
+ }
109
+ /** Make a raw SVG etIcon-compatible: fill its host, inherit currentColor, drop the license comment. */
110
+ function toIconData(rawSvg) {
111
+ return rawSvg
112
+ .replace(/<!--[\s\S]*?-->/g, '')
113
+ .replace('<svg ', '<svg width="100%" height="100%" fill="currentColor" ')
114
+ .replace(/\s+/g, ' ')
115
+ .trim();
116
+ }
117
+ /** `shield` + `light` -> `SHIELD_LIGHT` */
118
+ function constName(name, variant) {
119
+ return `${name}_${variant}`.replace(/[^a-z0-9]+/gi, '_').toUpperCase();
120
+ }
121
+ function regenerateCommand(schema) {
122
+ const parts = ['nx g @ethlete/components:icons'];
123
+ if (schema.configPath)
124
+ parts.push(`--configPath=${schema.configPath}`);
125
+ if (schema.outputPath)
126
+ parts.push(`--outputPath=${schema.outputPath}`);
127
+ if (schema.typesOutputPath)
128
+ parts.push(`--typesOutputPath=${schema.typesOutputPath}`);
129
+ if (schema.source && schema.source !== 'auto')
130
+ parts.push(`--source=${schema.source}`);
131
+ return parts.join(' ');
132
+ }
133
+ function generateIconsFile(icons, schema, source) {
134
+ const header = `/* eslint-disable */
135
+ // AUTO-GENERATED by @ethlete/components:icons from "${source}" — DO NOT EDIT.
136
+ // Regenerate by running:
137
+ // ${regenerateCommand(schema)}
138
+ import type { IconDefinition } from '@ethlete/components';
139
+ `;
140
+ const consts = icons
141
+ .map((icon) => `export const ${constName(icon.name, icon.variant)}: IconDefinition = {\n` +
142
+ ` name: '${icon.name}',\n` +
143
+ ` variant: '${icon.variant}',\n` +
144
+ ` data: \`${icon.data}\`,\n` +
145
+ `};`)
146
+ .join('\n\n');
147
+ const aggregate = `export const GENERATED_ICONS = [\n${icons
148
+ .map((icon) => ` ${constName(icon.name, icon.variant)},`)
149
+ .join('\n')}\n] as const;`;
150
+ return `${header}\n${consts}\n\n${aggregate}\n`;
151
+ }
152
+ function generateTypesFile(icons, schema) {
153
+ const names = [...new Set(icons.map((i) => i.name))]
154
+ .sort()
155
+ .map((n) => `'${n}'`)
156
+ .join(' | ');
157
+ const variants = [...new Set(icons.map((i) => i.variant))]
158
+ .sort()
159
+ .map((v) => `'${v}'`)
160
+ .join(' | ');
161
+ return `/*
162
+ * Auto-generated by @ethlete/components:icons
163
+ * DO NOT EDIT THIS FILE MANUALLY
164
+ *
165
+ * Regenerate by running:
166
+ * ${regenerateCommand(schema)}
167
+ */
168
+
169
+ declare module '@ethlete/components' {
170
+ interface EthleteIconNameRegistry {
171
+ name: ${names};
172
+ }
173
+
174
+ interface EthleteIconVariantRegistry {
175
+ name: ${variants};
176
+ }
177
+ }
178
+
179
+ export {};
180
+ `;
181
+ }
182
+ //#endregion
183
+ //# sourceMappingURL=generator.js.map
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "et-icons",
4
+ "title": "Ethlete Icon Generator",
5
+ "description": "Generates etIcon IconDefinitions and typed names from an installed SVG icon source. Auto-detects Font Awesome (pro, then free).",
6
+ "type": "object",
7
+ "properties": {
8
+ "configPath": {
9
+ "type": "string",
10
+ "description": "Path to the icons config JSON (relative to workspace root). Lists the icons/variants to generate.",
11
+ "default": "src/icons.json",
12
+ "x-prompt": "Where is your icons config file?"
13
+ },
14
+ "outputPath": {
15
+ "type": "string",
16
+ "description": "Where to write the generated IconDefinition constants (.ts).",
17
+ "default": "src/generated/et-icons.ts"
18
+ },
19
+ "typesOutputPath": {
20
+ "type": "string",
21
+ "description": "Where to write the generated name/variant type augmentation (.d.ts). Defaults to 'et-icon-registry.d.ts' next to outputPath."
22
+ },
23
+ "source": {
24
+ "type": "string",
25
+ "description": "Icon source package to read SVGs from (expects a `svgs/<variant>/<name>.svg` layout). 'auto' detects Font Awesome pro then free.",
26
+ "default": "auto"
27
+ },
28
+ "skipFormat": {
29
+ "type": "boolean",
30
+ "description": "Skip formatting files after generation.",
31
+ "default": false
32
+ }
33
+ },
34
+ "required": []
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethlete/components",
3
- "version": "0.1.0-next.14",
3
+ "version": "0.1.0-next.16",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
@@ -13,8 +13,15 @@
13
13
  "@angular/router": "22.0.5",
14
14
  "@ethlete/core": "^5.0.0-beta.11",
15
15
  "@floating-ui/dom": "1.7.6",
16
+ "@nx/devkit": "23.1.0-beta.7",
16
17
  "rxjs": "7.8.2"
17
18
  },
19
+ "peerDependenciesMeta": {
20
+ "@nx/devkit": {
21
+ "optional": true
22
+ }
23
+ },
24
+ "generators": "./generators/generators.json",
18
25
  "module": "fesm2022/ethlete-components.mjs",
19
26
  "typings": "types/ethlete-components.d.ts",
20
27
  "exports": {