@ethlete/components 0.1.0-next.9 → 1.0.0-next.18

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,191 @@
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_VARIANTS = ['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(` { "variants": ["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
+ // Prefer the `variants` list; fall back to `['solid']`.
37
+ const defaultVariants = config.variants?.length ? config.variants : DEFAULT_VARIANTS;
38
+ // Step 2: Resolve the SVG source package. `'auto'` (the default, from the option or the
39
+ // config) means auto-detect; an explicit package name overrides it.
40
+ const configuredSource = schema.source && schema.source !== 'auto' ? schema.source : config.source;
41
+ const requestedSource = configuredSource && configuredSource !== 'auto' ? configuredSource : undefined;
42
+ const source = resolveSource(requestedSource);
43
+ if (!source) {
44
+ logger.error('āŒ Could not find an icon source. Install @fortawesome/fontawesome-pro (or -free), or set "source".');
45
+ return;
46
+ }
47
+ logger.log(`šŸ“¦ Using icon source: ${source.package}`);
48
+ // Step 3: Read + transform each icon's SVG.
49
+ const requested = normalizeIcons(config.icons, defaultVariants);
50
+ const resolved = [];
51
+ const missing = [];
52
+ for (const icon of requested) {
53
+ const svgPath = join(source.svgsDir, icon.variant, `${icon.name}.svg`);
54
+ if (!existsSync(svgPath)) {
55
+ missing.push(`${icon.name} (${icon.variant})`);
56
+ continue;
57
+ }
58
+ resolved.push({ ...icon, data: toIconData(readFileSync(svgPath, 'utf-8')) });
59
+ }
60
+ if (missing.length) {
61
+ logger.warn(`āš ļø ${missing.length} icon(s) not found in ${source.package} and skipped:\n - ${missing.join('\n - ')}`);
62
+ }
63
+ if (resolved.length === 0) {
64
+ logger.error('āŒ No icons could be resolved. Nothing was written.');
65
+ return;
66
+ }
67
+ // Step 4: Write the IconDefinition constants.
68
+ tree.write(outputPath, generateIconsFile(resolved, schema, source.package));
69
+ logger.log(`āœ… Generated ${resolved.length} icon(s) at: ${outputPath}`);
70
+ // Step 5: Write the type augmentation so `etIcon`/`variant` are checked against these names.
71
+ tree.write(typesOutputPath, generateTypesFile(resolved, schema));
72
+ logger.log(`āœ… Generated icon name types at: ${typesOutputPath}`);
73
+ if (!schema.skipFormat) {
74
+ await formatFiles(tree);
75
+ }
76
+ logger.log('\nāœ… Icon generation completed successfully!\n');
77
+ }
78
+ //#region Helpers
79
+ function resolveSource(requested) {
80
+ const candidates = requested ? [requested] : KNOWN_SOURCES;
81
+ for (const pkg of candidates) {
82
+ const svgsDir = join(workspaceRoot, 'node_modules', pkg, 'svgs');
83
+ if (existsSync(svgsDir)) {
84
+ return { package: pkg, svgsDir };
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+ function normalizeIcons(entries, defaultVariants) {
90
+ const seen = new Set();
91
+ const result = [];
92
+ const add = (name, variant) => {
93
+ const key = `${name}::${variant}`;
94
+ if (!seen.has(key)) {
95
+ seen.add(key);
96
+ result.push({ name, variant });
97
+ }
98
+ };
99
+ for (const entry of entries) {
100
+ // A bare string uses the config's default variants. An object may override with its own
101
+ // `variants` list (or the singular `variant`); otherwise it also gets the defaults.
102
+ const variants = typeof entry === 'string'
103
+ ? defaultVariants
104
+ : (entry.variants ?? (entry.variant ? [entry.variant] : defaultVariants));
105
+ const name = typeof entry === 'string' ? entry : entry.name;
106
+ for (const variant of variants) {
107
+ add(name, variant);
108
+ }
109
+ }
110
+ // Stable ordering keeps regeneration diffs minimal.
111
+ return result.sort((a, b) => `${a.name}::${a.variant}`.localeCompare(`${b.name}::${b.variant}`));
112
+ }
113
+ /** Make a raw SVG etIcon-compatible: fill its host, inherit currentColor, drop the license comment. */
114
+ function toIconData(rawSvg) {
115
+ return rawSvg
116
+ .replace(/<!--[\s\S]*?-->/g, '')
117
+ .replace('<svg ', '<svg width="100%" height="100%" fill="currentColor" ')
118
+ .replace(/\s+/g, ' ')
119
+ .trim();
120
+ }
121
+ /** `shield` + `light` -> `SHIELD_LIGHT` */
122
+ function constName(name, variant) {
123
+ return `${name}_${variant}`.replace(/[^a-z0-9]+/gi, '_').toUpperCase();
124
+ }
125
+ function regenerateCommand(schema) {
126
+ const parts = ['nx g @ethlete/components:icons'];
127
+ if (schema.configPath)
128
+ parts.push(`--configPath=${schema.configPath}`);
129
+ if (schema.outputPath)
130
+ parts.push(`--outputPath=${schema.outputPath}`);
131
+ if (schema.typesOutputPath)
132
+ parts.push(`--typesOutputPath=${schema.typesOutputPath}`);
133
+ if (schema.source && schema.source !== 'auto')
134
+ parts.push(`--source=${schema.source}`);
135
+ return parts.join(' ');
136
+ }
137
+ function generateIconsFile(icons, schema, source) {
138
+ const header = `/* eslint-disable */
139
+ /*
140
+ * Auto-generated by @ethlete/components:icons from "${source}"
141
+ * DO NOT EDIT THIS FILE MANUALLY
142
+ *
143
+ * Regenerate by running:
144
+ * ${regenerateCommand(schema)}
145
+ */
146
+ import type { IconDefinition } from '@ethlete/components';
147
+ `;
148
+ const consts = icons
149
+ .map((icon) => `export const ${constName(icon.name, icon.variant)}: IconDefinition = {\n` +
150
+ ` name: '${icon.name}',\n` +
151
+ ` variant: '${icon.variant}',\n` +
152
+ ` data: \`${icon.data}\`,\n` +
153
+ `};`)
154
+ .join('\n\n');
155
+ const aggregate = `export const GENERATED_ICONS = [\n${icons
156
+ .map((icon) => ` ${constName(icon.name, icon.variant)},`)
157
+ .join('\n')}\n] as const;`;
158
+ return `${header}\n${consts}\n\n${aggregate}\n`;
159
+ }
160
+ function generateTypesFile(icons, schema) {
161
+ const names = [...new Set(icons.map((i) => i.name))]
162
+ .sort()
163
+ .map((n) => `'${n}'`)
164
+ .join(' | ');
165
+ const variants = [...new Set(icons.map((i) => i.variant))]
166
+ .sort()
167
+ .map((v) => `'${v}'`)
168
+ .join(' | ');
169
+ return `/*
170
+ * Auto-generated by @ethlete/components:icons
171
+ * DO NOT EDIT THIS FILE MANUALLY
172
+ *
173
+ * Regenerate by running:
174
+ * ${regenerateCommand(schema)}
175
+ */
176
+
177
+ declare module '@ethlete/components' {
178
+ interface EthleteIconNameRegistry {
179
+ name: ${names};
180
+ }
181
+
182
+ interface EthleteIconVariantRegistry {
183
+ name: ${variants};
184
+ }
185
+ }
186
+
187
+ export {};
188
+ `;
189
+ }
190
+ //#endregion
191
+ //# 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,20 +1,28 @@
1
1
  {
2
2
  "name": "@ethlete/components",
3
- "version": "0.1.0-next.9",
3
+ "version": "1.0.0-next.18",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
7
- "@analogjs/vitest-angular": "2.5.1",
8
- "@angular/common": "21.2.13",
9
- "@angular/compiler": "21.2.13",
10
- "@angular/core": "21.2.13",
11
- "@angular/forms": "21.2.13",
12
- "@angular/platform-browser": "21.2.13",
13
- "@angular/router": "21.2.13",
7
+ "@analogjs/vitest-angular": "2.6.2",
8
+ "@angular/common": "22.0.5",
9
+ "@angular/compiler": "22.0.5",
10
+ "@angular/core": "22.0.5",
11
+ "@angular/forms": "22.0.5",
12
+ "@angular/platform-browser": "22.0.5",
13
+ "@angular/router": "22.0.5",
14
14
  "@ethlete/core": "^5.0.0-beta.11",
15
+ "@ethlete/query": "^6.0.0-beta.8",
15
16
  "@floating-ui/dom": "1.7.6",
17
+ "@nx/devkit": "23.1.0-beta.7",
16
18
  "rxjs": "7.8.2"
17
19
  },
20
+ "peerDependenciesMeta": {
21
+ "@nx/devkit": {
22
+ "optional": true
23
+ }
24
+ },
25
+ "generators": "./generators/generators.json",
18
26
  "module": "fesm2022/ethlete-components.mjs",
19
27
  "typings": "types/ethlete-components.d.ts",
20
28
  "exports": {