@truenas/ui-components 0.1.46 → 0.1.47
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/fesm2022/truenas-ui-components.mjs +39 -3
- package/fesm2022/truenas-ui-components.mjs.map +1 -1
- package/package.json +1 -1
- package/scripts/icon-sprite/__fixtures__/consumer-templates/basic.component.html +30 -0
- package/scripts/icon-sprite/__fixtures__/custom-icons/brand.svg +1 -0
- package/scripts/icon-sprite/__fixtures__/custom-icons/logo.svg +1 -0
- package/scripts/icon-sprite/__fixtures__/forwarding-components/multi-icon.component.html +7 -0
- package/scripts/icon-sprite/__fixtures__/forwarding-components/multi-icon.component.ts +15 -0
- package/scripts/icon-sprite/__fixtures__/forwarding-components/no-library.component.html +3 -0
- package/scripts/icon-sprite/__fixtures__/forwarding-components/no-library.component.ts +11 -0
- package/scripts/icon-sprite/__fixtures__/forwarding-components/not-forwarding.component.html +1 -0
- package/scripts/icon-sprite/__fixtures__/forwarding-components/not-forwarding.component.ts +10 -0
- package/scripts/icon-sprite/__fixtures__/forwarding-components/single-icon.component.html +3 -0
- package/scripts/icon-sprite/__fixtures__/forwarding-components/single-icon.component.ts +13 -0
- package/scripts/icon-sprite/__fixtures__/marker-sources/icons.ts +24 -0
- package/scripts/icon-sprite/cli-main.ts +27 -8
- package/scripts/icon-sprite/generate-sprite.ts +15 -4
- package/scripts/icon-sprite/jest.config.ts +14 -0
- package/scripts/icon-sprite/lib/find-icons-in-forwarding-components.spec.ts +77 -0
- package/scripts/icon-sprite/lib/find-icons-in-forwarding-components.ts +209 -0
- package/scripts/icon-sprite/lib/find-icons-in-templates.spec.ts +119 -0
- package/scripts/icon-sprite/lib/find-icons-in-templates.ts +66 -17
- package/scripts/icon-sprite/lib/find-icons-with-marker.spec.ts +58 -0
- package/scripts/icon-sprite/lib/find-icons-with-marker.ts +37 -22
- package/scripts/icon-sprite/lib/validate-icons.spec.ts +170 -0
- package/scripts/icon-sprite/lib/validate-icons.ts +185 -0
- package/scripts/icon-sprite/tsconfig.json +14 -0
- package/types/truenas-ui-components.d.ts +42 -4
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { findIconsInTemplates } from './find-icons-in-templates';
|
|
3
|
+
import type { ForwardingComponentMapping } from './find-icons-in-forwarding-components';
|
|
4
|
+
|
|
5
|
+
const CONSUMER_FIXTURES = path.resolve(__dirname, '../__fixtures__/consumer-templates');
|
|
6
|
+
|
|
7
|
+
const FORWARDING_MAPPINGS: ForwardingComponentMapping[] = [
|
|
8
|
+
{
|
|
9
|
+
selector: 'tn-empty',
|
|
10
|
+
iconSlots: [{ iconAttribute: 'icon', libraryAttribute: 'iconLibrary', defaultLibrary: 'mdi' }],
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
selector: 'tn-input',
|
|
14
|
+
iconSlots: [
|
|
15
|
+
{ iconAttribute: 'prefixIcon', libraryAttribute: 'prefixIconLibrary' },
|
|
16
|
+
{ iconAttribute: 'suffixIcon', libraryAttribute: 'suffixIconLibrary' },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
selector: 'tn-chip',
|
|
21
|
+
iconSlots: [{ iconAttribute: 'icon' }],
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
describe('findIconsInTemplates', () => {
|
|
26
|
+
describe('tn-icon scanning (existing behavior)', () => {
|
|
27
|
+
it('should find static tn-icon names with library prefix', () => {
|
|
28
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES);
|
|
29
|
+
expect(icons.has('mdi-folder')).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should find tn-icon ternary expressions', () => {
|
|
33
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES);
|
|
34
|
+
expect(icons.has('mdi-chevron-down')).toBe(true);
|
|
35
|
+
expect(icons.has('mdi-chevron-right')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should find material library icons', () => {
|
|
39
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES);
|
|
40
|
+
expect(icons.has('mat-check_circle')).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should skip registry format icons (with colon)', () => {
|
|
44
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES);
|
|
45
|
+
const registryIcons = [...icons].filter(i => i.includes(':'));
|
|
46
|
+
expect(registryIcons).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should find tn-icon-button names', () => {
|
|
50
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES);
|
|
51
|
+
expect(icons.has('mdi-close')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('forwarding component scanning', () => {
|
|
56
|
+
it('should find icons from tn-empty static attributes', () => {
|
|
57
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES, undefined, FORWARDING_MAPPINGS);
|
|
58
|
+
expect(icons.has('mdi-inbox')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should find icons from tn-empty ternary with default library', () => {
|
|
62
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES, undefined, FORWARDING_MAPPINGS);
|
|
63
|
+
// tn-empty has defaultLibrary: 'mdi', ternary values get prefixed
|
|
64
|
+
expect(icons.has('mdi-check')).toBe(true);
|
|
65
|
+
expect(icons.has('mdi-alert')).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should find icons from tn-input prefixIcon with explicit library', () => {
|
|
69
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES, undefined, FORWARDING_MAPPINGS);
|
|
70
|
+
expect(icons.has('mdi-search')).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should find icons from tn-input suffixIcon with material library', () => {
|
|
74
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES, undefined, FORWARDING_MAPPINGS);
|
|
75
|
+
expect(icons.has('mat-eye')).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should find icons from tn-chip (no library, no default)', () => {
|
|
79
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES, undefined, FORWARDING_MAPPINGS);
|
|
80
|
+
expect(icons.has('star')).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should not extract dynamic signal bindings', () => {
|
|
84
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES, undefined, FORWARDING_MAPPINGS);
|
|
85
|
+
// [icon]="dynamicIcon()" should NOT produce any icon
|
|
86
|
+
const dynamicArtifacts = [...icons].filter(i => i.includes('dynamic') || i.includes('Icon'));
|
|
87
|
+
expect(dynamicArtifacts).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should not find forwarding icons when mappings are not provided', () => {
|
|
91
|
+
const withMappings = findIconsInTemplates(CONSUMER_FIXTURES, undefined, FORWARDING_MAPPINGS);
|
|
92
|
+
const withoutMappings = findIconsInTemplates(CONSUMER_FIXTURES);
|
|
93
|
+
|
|
94
|
+
// Without mappings, tn-empty/tn-input/tn-chip icons should be missing
|
|
95
|
+
expect(withMappings.icons.size).toBeGreaterThan(withoutMappings.icons.size);
|
|
96
|
+
expect(withoutMappings.icons.has('mdi-inbox')).toBe(false);
|
|
97
|
+
expect(withoutMappings.icons.has('mdi-search')).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('skipIcons filtering', () => {
|
|
102
|
+
it('should exclude icons in the skip set', () => {
|
|
103
|
+
const skipIcons = new Set(['mdi-folder', 'mat-check_circle']);
|
|
104
|
+
const { icons } = findIconsInTemplates(CONSUMER_FIXTURES, skipIcons);
|
|
105
|
+
expect(icons.has('mdi-folder')).toBe(false);
|
|
106
|
+
expect(icons.has('mat-check_circle')).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('source tracking', () => {
|
|
111
|
+
it('should return source file paths for each icon', () => {
|
|
112
|
+
const { sources } = findIconsInTemplates(CONSUMER_FIXTURES);
|
|
113
|
+
expect(sources.has('mdi-folder')).toBe(true);
|
|
114
|
+
const folderSources = sources.get('mdi-folder')!;
|
|
115
|
+
expect(folderSources.length).toBeGreaterThan(0);
|
|
116
|
+
expect(folderSources[0]).toContain('basic.component.html');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -1,31 +1,69 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import fg from 'fast-glob';
|
|
3
3
|
import * as cheerio from 'cheerio';
|
|
4
|
+
import type { ForwardingComponentMapping } from './find-icons-in-forwarding-components';
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
+
export interface ScanResult {
|
|
7
|
+
icons: Set<string>;
|
|
8
|
+
sources: Map<string, string[]>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function findIconsInTemplates(
|
|
12
|
+
path: string,
|
|
13
|
+
skipIcons?: Set<string>,
|
|
14
|
+
forwardingMappings?: ForwardingComponentMapping[],
|
|
15
|
+
): ScanResult {
|
|
6
16
|
const iconNames = new Set<string>();
|
|
17
|
+
const sources = new Map<string, string[]>();
|
|
7
18
|
|
|
8
19
|
const templates = fg.sync(`${path}/**/*.html`);
|
|
9
20
|
|
|
21
|
+
const addIcon = (iconName: string, templateFile: string) => {
|
|
22
|
+
iconNames.add(iconName);
|
|
23
|
+
const existing = sources.get(iconName) || [];
|
|
24
|
+
existing.push(templateFile);
|
|
25
|
+
sources.set(iconName, existing);
|
|
26
|
+
};
|
|
27
|
+
|
|
10
28
|
templates.forEach((template) => {
|
|
11
29
|
const content = fs.readFileSync(template, 'utf-8');
|
|
12
30
|
const parsedTemplate = cheerio.load(content);
|
|
13
31
|
|
|
14
|
-
//
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
// Generic function to extract icon names from an element's attributes
|
|
33
|
+
const processElement = (
|
|
34
|
+
el: cheerio.Element,
|
|
35
|
+
iconAttr: string,
|
|
36
|
+
libraryAttr: string | undefined,
|
|
37
|
+
defaultLibrary: string | undefined,
|
|
38
|
+
) => {
|
|
39
|
+
// Check both static and [bound] attributes (Angular binding syntax)
|
|
40
|
+
// Cheerio lowercases HTML attribute names, so look up the lowercase form
|
|
41
|
+
const iconAttrLower = iconAttr.toLowerCase();
|
|
42
|
+
const staticName = parsedTemplate(el).attr(iconAttrLower);
|
|
43
|
+
const boundName = parsedTemplate(el).attr(`[${iconAttrLower}]`);
|
|
44
|
+
const libraryAttrLower = libraryAttr?.toLowerCase();
|
|
45
|
+
const library = libraryAttrLower
|
|
46
|
+
? parsedTemplate(el).attr(libraryAttrLower) || parsedTemplate(el).attr(`[${libraryAttrLower}]`)
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
// Resolve library: use explicit value, fall back to default
|
|
50
|
+
let resolvedLibrary = defaultLibrary;
|
|
51
|
+
if (library) {
|
|
52
|
+
// Handle both static library="mdi" and bound [library]="'mdi'"
|
|
53
|
+
const staticLib = library.replace(/^['"]|['"]$/g, '');
|
|
54
|
+
if (/^[a-z]+$/.test(staticLib)) {
|
|
55
|
+
resolvedLibrary = staticLib;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
20
58
|
|
|
21
59
|
const extractedNames: string[] = [];
|
|
22
60
|
|
|
23
|
-
// Handle static name attribute:
|
|
61
|
+
// Handle static name attribute: icon="folder"
|
|
24
62
|
if (staticName) {
|
|
25
63
|
extractedNames.push(staticName);
|
|
26
64
|
}
|
|
27
65
|
|
|
28
|
-
// Handle bound name attribute: [
|
|
66
|
+
// Handle bound name attribute: [icon]="expression"
|
|
29
67
|
// Extract string literals from the expression
|
|
30
68
|
if (boundName) {
|
|
31
69
|
// Match string literals that are values, not comparison operands
|
|
@@ -46,7 +84,7 @@ export function findIconsInTemplates(path: string, skipIcons?: Set<string>): Set
|
|
|
46
84
|
}
|
|
47
85
|
|
|
48
86
|
// Also handle simple string literals (no ternary, no comparison)
|
|
49
|
-
// e.g., [
|
|
87
|
+
// e.g., [icon]="'folder'" (though this is unusual)
|
|
50
88
|
if (!boundName.includes('?') && !boundName.includes('=')) {
|
|
51
89
|
const simpleStringRegex = /^['"]([^'"]+)['"]$/;
|
|
52
90
|
const simpleMatch = boundName.match(simpleStringRegex);
|
|
@@ -72,13 +110,13 @@ export function findIconsInTemplates(path: string, skipIcons?: Set<string>): Set
|
|
|
72
110
|
let finalIconName: string;
|
|
73
111
|
|
|
74
112
|
// Handle library attribute - prefix the icon name with library prefix
|
|
75
|
-
if (
|
|
113
|
+
if (resolvedLibrary === 'mdi' && !iconName.startsWith('mdi-')) {
|
|
76
114
|
finalIconName = `mdi-${iconName}`;
|
|
77
|
-
} else if (
|
|
115
|
+
} else if (resolvedLibrary === 'custom' && !iconName.startsWith('app-') && !iconName.startsWith('tn-')) {
|
|
78
116
|
// Consumer custom icons get app- prefix
|
|
79
117
|
// (Library templates should never use library="custom", they use libIconMarker() instead)
|
|
80
118
|
finalIconName = `app-${iconName}`;
|
|
81
|
-
} else if (
|
|
119
|
+
} else if (resolvedLibrary === 'material' && !iconName.startsWith('mat-')) {
|
|
82
120
|
finalIconName = `mat-${iconName}`; // Material icons get mat- prefix
|
|
83
121
|
} else {
|
|
84
122
|
finalIconName = iconName;
|
|
@@ -89,20 +127,31 @@ export function findIconsInTemplates(path: string, skipIcons?: Set<string>): Set
|
|
|
89
127
|
return;
|
|
90
128
|
}
|
|
91
129
|
|
|
92
|
-
|
|
130
|
+
addIcon(finalIconName, template);
|
|
93
131
|
});
|
|
94
132
|
};
|
|
95
133
|
|
|
96
134
|
// Scan tn-icon elements
|
|
97
135
|
parsedTemplate('tn-icon').each((_, iconTag) => {
|
|
98
|
-
|
|
136
|
+
processElement(iconTag, 'name', 'library', undefined);
|
|
99
137
|
});
|
|
100
138
|
|
|
101
139
|
// Scan tn-icon-button elements (they also have name and library attributes)
|
|
102
140
|
parsedTemplate('tn-icon-button').each((_, iconTag) => {
|
|
103
|
-
|
|
141
|
+
processElement(iconTag, 'name', 'library', undefined);
|
|
104
142
|
});
|
|
143
|
+
|
|
144
|
+
// Scan icon-forwarding components
|
|
145
|
+
if (forwardingMappings) {
|
|
146
|
+
for (const mapping of forwardingMappings) {
|
|
147
|
+
parsedTemplate(mapping.selector).each((_, el) => {
|
|
148
|
+
for (const slot of mapping.iconSlots) {
|
|
149
|
+
processElement(el, slot.iconAttribute, slot.libraryAttribute, slot.defaultLibrary);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
105
154
|
});
|
|
106
155
|
|
|
107
|
-
return iconNames;
|
|
156
|
+
return { icons: iconNames, sources };
|
|
108
157
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { findIconsWithMarker } from './find-icons-with-marker';
|
|
3
|
+
|
|
4
|
+
const MARKER_FIXTURES = path.resolve(__dirname, '../__fixtures__/marker-sources');
|
|
5
|
+
|
|
6
|
+
describe('findIconsWithMarker', () => {
|
|
7
|
+
it('should find tnIconMarker calls with mdi library', () => {
|
|
8
|
+
const { icons } = findIconsWithMarker(MARKER_FIXTURES);
|
|
9
|
+
expect(icons.has('mdi-content-save')).toBe(true);
|
|
10
|
+
expect(icons.has('mdi-delete')).toBe(true);
|
|
11
|
+
expect(icons.has('mdi-pencil')).toBe(true);
|
|
12
|
+
expect(icons.has('mdi-trash-can')).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should find tnIconMarker calls with material library', () => {
|
|
16
|
+
const { icons } = findIconsWithMarker(MARKER_FIXTURES);
|
|
17
|
+
expect(icons.has('mat-check_circle')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should find tnIconMarker calls with custom library', () => {
|
|
21
|
+
const { icons } = findIconsWithMarker(MARKER_FIXTURES);
|
|
22
|
+
expect(icons.has('app-my-logo')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should find tnIconMarker calls without library', () => {
|
|
26
|
+
const { icons } = findIconsWithMarker(MARKER_FIXTURES);
|
|
27
|
+
expect(icons.has('some-icon')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should find libIconMarker calls with tn- prefix', () => {
|
|
31
|
+
const { icons } = findIconsWithMarker(MARKER_FIXTURES);
|
|
32
|
+
expect(icons.has('tn-dataset')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should skip icons in the skip set', () => {
|
|
36
|
+
const skipIcons = new Set(['mdi-content-save', 'tn-dataset']);
|
|
37
|
+
const { icons } = findIconsWithMarker(MARKER_FIXTURES, skipIcons);
|
|
38
|
+
expect(icons.has('mdi-content-save')).toBe(false);
|
|
39
|
+
expect(icons.has('tn-dataset')).toBe(false);
|
|
40
|
+
// Others should still be found
|
|
41
|
+
expect(icons.has('mdi-delete')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('source tracking', () => {
|
|
45
|
+
it('should return source file paths for each icon', () => {
|
|
46
|
+
const { sources } = findIconsWithMarker(MARKER_FIXTURES);
|
|
47
|
+
expect(sources.has('mdi-content-save')).toBe(true);
|
|
48
|
+
const saveSources = sources.get('mdi-content-save')!;
|
|
49
|
+
expect(saveSources.length).toBeGreaterThan(0);
|
|
50
|
+
expect(saveSources[0]).toContain('icons.ts');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return empty set for directory with no markers', () => {
|
|
55
|
+
const { icons } = findIconsWithMarker(path.resolve(__dirname, '../__fixtures__/custom-icons'));
|
|
56
|
+
expect(icons.size).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -1,22 +1,46 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import type { ScanResult } from './find-icons-in-templates';
|
|
2
3
|
|
|
3
|
-
export function findIconsWithMarker(path: string, skipIcons?: Set<string>):
|
|
4
|
+
export function findIconsWithMarker(path: string, skipIcons?: Set<string>): ScanResult {
|
|
4
5
|
// Updated regex to capture tnIconMarker() and libIconMarker() calls with optional second parameter
|
|
5
6
|
// Matches: tnIconMarker('name') or tnIconMarker('name', 'library') or libIconMarker('tn-name')
|
|
6
|
-
const
|
|
7
|
+
const pattern = "(tn|lib)IconMarker\\('[^']+',?\\s*'?[^'\\)]*'?\\)";
|
|
7
8
|
|
|
8
9
|
const icons = new Set<string>();
|
|
10
|
+
const sources = new Map<string, string[]>();
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
const addIcon = (iconName: string, sourceFile: string) => {
|
|
13
|
+
icons.add(iconName);
|
|
14
|
+
const existing = sources.get(iconName) || [];
|
|
15
|
+
existing.push(sourceFile);
|
|
16
|
+
sources.set(iconName, existing);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const result = spawnSync(
|
|
20
|
+
'grep',
|
|
21
|
+
['-rEo', pattern, '--include=*.ts', '--include=*.html', path],
|
|
22
|
+
{ encoding: 'utf-8' },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (result.status === 1 && !result.stderr) {
|
|
26
|
+
// grep returns exit code 1 when no matches found
|
|
27
|
+
return { icons, sources };
|
|
28
|
+
}
|
|
29
|
+
if (result.status !== 0 && result.status !== 1) {
|
|
30
|
+
throw new Error(`grep failed: ${result.stderr}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
result.stdout
|
|
34
|
+
.split('\n')
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.forEach((line) => {
|
|
37
|
+
// grep output format: "filepath:match"
|
|
38
|
+
const colonIdx = line.indexOf(':');
|
|
39
|
+
if (colonIdx === -1) {
|
|
18
40
|
return;
|
|
19
41
|
}
|
|
42
|
+
const sourceFile = line.substring(0, colonIdx);
|
|
43
|
+
const match = line.substring(colonIdx + 1);
|
|
20
44
|
|
|
21
45
|
// Extract icon name (first parameter)
|
|
22
46
|
const iconNameMatch = /'([^']+)'/.exec(match);
|
|
@@ -47,17 +71,8 @@ export function findIconsWithMarker(path: string, skipIcons?: Set<string>): Set<
|
|
|
47
71
|
return;
|
|
48
72
|
}
|
|
49
73
|
|
|
50
|
-
|
|
74
|
+
addIcon(iconName, sourceFile);
|
|
51
75
|
});
|
|
52
|
-
} catch (error: any) {
|
|
53
|
-
// grep returns exit code 1 when no matches are found, which is not an error
|
|
54
|
-
if (error.status === 1 && !error.stderr) {
|
|
55
|
-
// No matches found, return empty set
|
|
56
|
-
return icons;
|
|
57
|
-
}
|
|
58
|
-
// Re-throw actual errors
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
61
76
|
|
|
62
|
-
return icons;
|
|
77
|
+
return { icons, sources };
|
|
63
78
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { validateIcons, printValidationReport } from './validate-icons';
|
|
5
|
+
import type { ResolvedSpriteConfig } from '../sprite-config-interface';
|
|
6
|
+
|
|
7
|
+
const FIXTURES_BASE = path.resolve(__dirname, '../__fixtures__');
|
|
8
|
+
|
|
9
|
+
function createTempConfig(overrides: Partial<ResolvedSpriteConfig> = {}): ResolvedSpriteConfig {
|
|
10
|
+
return {
|
|
11
|
+
projectRoot: FIXTURES_BASE,
|
|
12
|
+
srcDirs: ['./consumer-templates', './marker-sources'],
|
|
13
|
+
outputDir: './output',
|
|
14
|
+
spriteUrlPath: 'assets/tn-icons',
|
|
15
|
+
customIconsDir: overrides.customIconsDir ?? null,
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeSpriteConfig(dir: string, icons: string[]): void {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
fs.writeFileSync(
|
|
23
|
+
path.join(dir, 'sprite-config.json'),
|
|
24
|
+
JSON.stringify({ iconUrl: 'sprite.svg?v=abc', icons }),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('validateIcons', () => {
|
|
29
|
+
let tempDir: string;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'icon-validate-'));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should report missing sprite config', () => {
|
|
40
|
+
const config = createTempConfig({ outputDir: path.join(tempDir, 'nonexistent') });
|
|
41
|
+
const result = validateIcons(config);
|
|
42
|
+
|
|
43
|
+
expect(result.spriteConfigFound).toBe(false);
|
|
44
|
+
expect(result.spriteIcons.size).toBe(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should detect icons missing from sprite', () => {
|
|
48
|
+
const outputDir = path.join(tempDir, 'output');
|
|
49
|
+
// Write a sprite config with only some icons
|
|
50
|
+
writeSpriteConfig(outputDir, ['mdi-folder']);
|
|
51
|
+
|
|
52
|
+
const config = createTempConfig({ outputDir });
|
|
53
|
+
const result = validateIcons(config);
|
|
54
|
+
|
|
55
|
+
expect(result.spriteConfigFound).toBe(true);
|
|
56
|
+
expect(result.missingFromSprite.size).toBeGreaterThan(0);
|
|
57
|
+
// mdi-folder is in the sprite, so it should NOT be missing
|
|
58
|
+
expect(result.missingFromSprite.has('mdi-folder')).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should detect stale icons in sprite', () => {
|
|
62
|
+
const outputDir = path.join(tempDir, 'output');
|
|
63
|
+
writeSpriteConfig(outputDir, ['mdi-folder', 'mdi-nonexistent-icon']);
|
|
64
|
+
|
|
65
|
+
const config = createTempConfig({ outputDir });
|
|
66
|
+
const result = validateIcons(config);
|
|
67
|
+
|
|
68
|
+
expect(result.staleInSprite.has('mdi-nonexistent-icon')).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should report clean when sprite matches scanned icons', () => {
|
|
72
|
+
const outputDir = path.join(tempDir, 'output');
|
|
73
|
+
// First, scan to get the full set
|
|
74
|
+
const config = createTempConfig({ outputDir });
|
|
75
|
+
const scan = validateIcons(config);
|
|
76
|
+
// Now write a sprite config with exactly those icons
|
|
77
|
+
writeSpriteConfig(outputDir, [...scan.scannedIcons]);
|
|
78
|
+
|
|
79
|
+
const result = validateIcons(config);
|
|
80
|
+
expect(result.missingFromSprite.size).toBe(0);
|
|
81
|
+
expect(result.staleInSprite.size).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should include custom icons in scanned set', () => {
|
|
85
|
+
const outputDir = path.join(tempDir, 'output');
|
|
86
|
+
writeSpriteConfig(outputDir, ['tn-logo', 'tn-brand']);
|
|
87
|
+
|
|
88
|
+
const config = createTempConfig({
|
|
89
|
+
outputDir,
|
|
90
|
+
customIconsDir: './custom-icons',
|
|
91
|
+
});
|
|
92
|
+
const result = validateIcons(config);
|
|
93
|
+
|
|
94
|
+
expect(result.scannedIcons.has('tn-logo')).toBe(true);
|
|
95
|
+
expect(result.scannedIcons.has('tn-brand')).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('printValidationReport', () => {
|
|
100
|
+
let consoleOutput: string[];
|
|
101
|
+
const originalLog = console.log;
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
consoleOutput = [];
|
|
105
|
+
console.log = (...args: unknown[]) => {
|
|
106
|
+
consoleOutput.push(args.map(String).join(' '));
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
console.log = originalLog;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should return 0 when sprite is clean', () => {
|
|
115
|
+
const exitCode = printValidationReport({
|
|
116
|
+
scannedIcons: new Set(['mdi-folder']),
|
|
117
|
+
spriteIcons: new Set(['mdi-folder']),
|
|
118
|
+
missingFromSprite: new Set(),
|
|
119
|
+
staleInSprite: new Set(),
|
|
120
|
+
sources: new Map(),
|
|
121
|
+
spriteConfigFound: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(exitCode).toBe(0);
|
|
125
|
+
expect(consoleOutput.some(line => line.includes('up to date'))).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should return 1 when icons are missing', () => {
|
|
129
|
+
const exitCode = printValidationReport({
|
|
130
|
+
scannedIcons: new Set(['mdi-folder', 'mdi-new']),
|
|
131
|
+
spriteIcons: new Set(['mdi-folder']),
|
|
132
|
+
missingFromSprite: new Set(['mdi-new']),
|
|
133
|
+
staleInSprite: new Set(),
|
|
134
|
+
sources: new Map([['mdi-new', ['src/app/test.html']]]),
|
|
135
|
+
spriteConfigFound: true,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(exitCode).toBe(1);
|
|
139
|
+
expect(consoleOutput.some(line => line.includes('mdi-new'))).toBe(true);
|
|
140
|
+
expect(consoleOutput.some(line => line.includes('test.html'))).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return 1 when no sprite config found', () => {
|
|
144
|
+
const exitCode = printValidationReport({
|
|
145
|
+
scannedIcons: new Set(['mdi-folder']),
|
|
146
|
+
spriteIcons: new Set(),
|
|
147
|
+
missingFromSprite: new Set(),
|
|
148
|
+
staleInSprite: new Set(),
|
|
149
|
+
sources: new Map(),
|
|
150
|
+
spriteConfigFound: false,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(exitCode).toBe(1);
|
|
154
|
+
expect(consoleOutput.some(line => line.includes('No sprite-config.json'))).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should return 0 when only stale icons exist', () => {
|
|
158
|
+
const exitCode = printValidationReport({
|
|
159
|
+
scannedIcons: new Set(['mdi-folder']),
|
|
160
|
+
spriteIcons: new Set(['mdi-folder', 'mdi-old']),
|
|
161
|
+
missingFromSprite: new Set(),
|
|
162
|
+
staleInSprite: new Set(['mdi-old']),
|
|
163
|
+
sources: new Map(),
|
|
164
|
+
spriteConfigFound: true,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(exitCode).toBe(0);
|
|
168
|
+
expect(consoleOutput.some(line => line.includes('mdi-old'))).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|