Skip to content

Commit cb09d7d

Browse files
authored
feat: auto-discover software and configurations using Vite glob (#21)
Replace manual imports in software-catalog.js and configurations.js with Vite's import.meta.glob for automatic discovery of items from their folder structure. Benefits: - Adding new software/config only requires creating a single file - No more manual editing of aggregator files - Reduces maintenance burden and prevents missed updates Changes: - Refactor software-catalog.js to use import.meta.glob (~320 -> ~50 lines) - Refactor configurations.js to use import.meta.glob (~260 -> ~50 lines) - Add Jest test helpers using Node.js glob for test compatibility - Update tests to use async loading in beforeAll - Add glob as dev dependency
1 parent 7035514 commit cb09d7d

File tree

7 files changed

+495
-649
lines changed

7 files changed

+495
-649
lines changed
Lines changed: 112 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { describe, test, expect } from '@jest/globals';
2-
import { softwareCatalog } from '../src/data/software-catalog.js';
3-
import { configurations } from '../src/data/configurations.js';
1+
import { describe, test, expect, beforeAll } from '@jest/globals';
2+
import { loadTestSoftwareCatalog } from './test-helpers/load-software-catalog.js';
3+
import { loadTestConfigurations } from './test-helpers/load-configurations.js';
44
import { categories, configCategories } from '../src/data/categories.js';
55

6+
// Catalogs loaded asynchronously before tests run
7+
let softwareCatalog;
8+
let configurations;
9+
610
// Extract valid category IDs from categories.js
711
const VALID_SOFTWARE_CATEGORIES = categories.map(cat => cat.id);
812
const VALID_CONFIG_CATEGORIES = configCategories.map(cat => cat.id);
@@ -13,6 +17,12 @@ const isKebabCase = (str) => /^[a-z0-9]+(-[a-z0-9]+)*$/.test(str);
1317
const isHexColor = (str) => /^#[0-9A-Fa-f]{6}$/.test(str);
1418
const hasWingetFormat = (str) => str && str.includes('.');
1519

20+
// Load catalogs before all tests
21+
beforeAll(async () => {
22+
softwareCatalog = await loadTestSoftwareCatalog();
23+
configurations = await loadTestConfigurations();
24+
});
25+
1626
describe('Software Catalog Validation', () => {
1727
test('should have software items', () => {
1828
expect(softwareCatalog).toBeDefined();
@@ -26,59 +36,71 @@ describe('Software Catalog Validation', () => {
2636
expect(ids.length).toBe(uniqueIds.size);
2737
});
2838

29-
test.each(softwareCatalog)('software "$name" should have valid schema', (item) => {
30-
// Required fields
31-
expect(item.id).toBeDefined();
32-
expect(typeof item.id).toBe('string');
33-
expect(item.id.length).toBeGreaterThan(0);
39+
test('all software should have valid schema', () => {
40+
softwareCatalog.forEach((item) => {
41+
// Required fields
42+
expect(item.id).toBeDefined();
43+
expect(typeof item.id).toBe('string');
44+
expect(item.id.length).toBeGreaterThan(0);
3445

35-
expect(item.name).toBeDefined();
36-
expect(typeof item.name).toBe('string');
37-
expect(item.name.length).toBeGreaterThan(0);
46+
expect(item.name).toBeDefined();
47+
expect(typeof item.name).toBe('string');
48+
expect(item.name.length).toBeGreaterThan(0);
3849

39-
expect(item.description).toBeDefined();
40-
expect(typeof item.description).toBe('string');
41-
expect(item.description.length).toBeGreaterThan(0);
50+
expect(item.description).toBeDefined();
51+
expect(typeof item.description).toBe('string');
52+
expect(item.description.length).toBeGreaterThan(0);
4253

43-
expect(item.category).toBeDefined();
44-
expect(typeof item.category).toBe('string');
54+
expect(item.category).toBeDefined();
55+
expect(typeof item.category).toBe('string');
4556

46-
expect(item.wingetId).toBeDefined();
47-
expect(typeof item.wingetId).toBe('string');
48-
expect(item.wingetId.length).toBeGreaterThan(0);
57+
expect(item.wingetId).toBeDefined();
58+
expect(typeof item.wingetId).toBe('string');
59+
expect(item.wingetId.length).toBeGreaterThan(0);
4960

50-
expect(item.icon).toBeDefined();
51-
expect(typeof item.icon).toBe('string');
52-
expect(item.icon.length).toBeGreaterThan(0);
61+
expect(item.icon).toBeDefined();
62+
expect(typeof item.icon).toBe('string');
63+
expect(item.icon.length).toBeGreaterThan(0);
5364

54-
expect(item.popular).toBeDefined();
55-
expect(typeof item.popular).toBe('boolean');
65+
expect(item.popular).toBeDefined();
66+
expect(typeof item.popular).toBe('boolean');
5667

57-
expect(item.requiresAdmin).toBeDefined();
58-
expect(typeof item.requiresAdmin).toBe('boolean');
68+
expect(item.requiresAdmin).toBeDefined();
69+
expect(typeof item.requiresAdmin).toBe('boolean');
5970

60-
expect(item.license).toBeDefined();
61-
expect(typeof item.license).toBe('string');
71+
expect(item.license).toBeDefined();
72+
expect(typeof item.license).toBe('string');
73+
});
6274
});
6375

64-
test.each(softwareCatalog)('software "$name" ID should be kebab-case', (item) => {
65-
expect(isKebabCase(item.id)).toBe(true);
76+
test('all software IDs should be kebab-case', () => {
77+
softwareCatalog.forEach((item) => {
78+
expect(isKebabCase(item.id)).toBe(true);
79+
});
6680
});
6781

68-
test.each(softwareCatalog)('software "$name" should have valid category (ENUM)', (item) => {
69-
expect(VALID_SOFTWARE_CATEGORIES).toContain(item.category);
82+
test('all software should have valid category (ENUM)', () => {
83+
softwareCatalog.forEach((item) => {
84+
expect(VALID_SOFTWARE_CATEGORIES).toContain(item.category);
85+
});
7086
});
7187

72-
test.each(softwareCatalog)('software "$name" should have valid license (ENUM)', (item) => {
73-
expect(VALID_LICENSES).toContain(item.license);
88+
test('all software should have valid license (ENUM)', () => {
89+
softwareCatalog.forEach((item) => {
90+
expect(VALID_LICENSES).toContain(item.license);
91+
});
7492
});
7593

76-
test.each(softwareCatalog)('software "$name" wingetId should have valid format', (item) => {
77-
expect(hasWingetFormat(item.wingetId)).toBe(true);
94+
test('all software wingetId should have valid format', () => {
95+
softwareCatalog.forEach((item) => {
96+
expect(hasWingetFormat(item.wingetId)).toBe(true);
97+
});
7898
});
7999

80-
test.each(softwareCatalog.filter(item => item.iconColor))('software "$name" iconColor should be valid hex', (item) => {
81-
expect(isHexColor(item.iconColor)).toBe(true);
100+
test('all software with iconColor should have valid hex', () => {
101+
softwareCatalog.filter(item => item.iconColor).forEach((item) => {
102+
expect(isHexColor(item.iconColor)).toBe(true);
103+
});
82104
});
83105
});
84106

@@ -95,63 +117,77 @@ describe('Configurations Validation', () => {
95117
expect(ids.length).toBe(uniqueIds.size);
96118
});
97119

98-
test.each(configurations)('configuration "$name" should have valid schema', (item) => {
99-
// Required fields
100-
expect(item.id).toBeDefined();
101-
expect(typeof item.id).toBe('string');
102-
expect(item.id.length).toBeGreaterThan(0);
120+
test('all configurations should have valid schema', () => {
121+
configurations.forEach((item) => {
122+
// Required fields
123+
expect(item.id).toBeDefined();
124+
expect(typeof item.id).toBe('string');
125+
expect(item.id.length).toBeGreaterThan(0);
103126

104-
expect(item.name).toBeDefined();
105-
expect(typeof item.name).toBe('string');
106-
expect(item.name.length).toBeGreaterThan(0);
127+
expect(item.name).toBeDefined();
128+
expect(typeof item.name).toBe('string');
129+
expect(item.name.length).toBeGreaterThan(0);
107130

108-
expect(item.description).toBeDefined();
109-
expect(typeof item.description).toBe('string');
110-
expect(item.description.length).toBeGreaterThan(0);
131+
expect(item.description).toBeDefined();
132+
expect(typeof item.description).toBe('string');
133+
expect(item.description.length).toBeGreaterThan(0);
111134

112-
expect(item.category).toBeDefined();
113-
expect(typeof item.category).toBe('string');
135+
expect(item.category).toBeDefined();
136+
expect(typeof item.category).toBe('string');
114137

115-
expect(item.recommended).toBeDefined();
116-
expect(typeof item.recommended).toBe('boolean');
138+
expect(item.recommended).toBeDefined();
139+
expect(typeof item.recommended).toBe('boolean');
117140

118-
expect(item.requiresRestart).toBeDefined();
119-
expect(typeof item.requiresRestart).toBe('boolean');
141+
expect(item.requiresRestart).toBeDefined();
142+
expect(typeof item.requiresRestart).toBe('boolean');
120143

121-
expect(item.requiresAdmin).toBeDefined();
122-
expect(typeof item.requiresAdmin).toBe('boolean');
144+
expect(item.requiresAdmin).toBeDefined();
145+
expect(typeof item.requiresAdmin).toBe('boolean');
146+
});
123147
});
124148

125-
test.each(configurations)('configuration "$name" ID should be kebab-case', (item) => {
126-
expect(isKebabCase(item.id)).toBe(true);
149+
test('all configuration IDs should be kebab-case', () => {
150+
configurations.forEach((item) => {
151+
expect(isKebabCase(item.id)).toBe(true);
152+
});
127153
});
128154

129-
test.each(configurations)('configuration "$name" should have valid category (ENUM)', (item) => {
130-
expect(VALID_CONFIG_CATEGORIES).toContain(item.category);
155+
test('all configurations should have valid category (ENUM)', () => {
156+
configurations.forEach((item) => {
157+
expect(VALID_CONFIG_CATEGORIES).toContain(item.category);
158+
});
131159
});
132160

133-
test.each(configurations)('configuration "$name" should have at least one bat array', (item) => {
134-
const hasRegistryBat = Array.isArray(item.registryBat) && item.registryBat.length > 0;
135-
const hasCommandBat = Array.isArray(item.commandBat) && item.commandBat.length > 0;
136-
expect(hasRegistryBat || hasCommandBat).toBe(true);
161+
test('all configurations should have at least one bat array', () => {
162+
configurations.forEach((item) => {
163+
const hasRegistryBat = Array.isArray(item.registryBat) && item.registryBat.length > 0;
164+
const hasCommandBat = Array.isArray(item.commandBat) && item.commandBat.length > 0;
165+
expect(hasRegistryBat || hasCommandBat).toBe(true);
166+
});
137167
});
138168

139-
test.each(configurations.filter(item => item.registryBat))('configuration "$name" registryBat should be array of strings', (item) => {
140-
expect(Array.isArray(item.registryBat)).toBe(true);
141-
item.registryBat.forEach(cmd => {
142-
expect(typeof cmd).toBe('string');
169+
test('all configurations with registryBat should have array of strings', () => {
170+
configurations.filter(item => item.registryBat).forEach((item) => {
171+
expect(Array.isArray(item.registryBat)).toBe(true);
172+
item.registryBat.forEach(cmd => {
173+
expect(typeof cmd).toBe('string');
174+
});
143175
});
144176
});
145177

146-
test.each(configurations.filter(item => item.commandBat))('configuration "$name" commandBat should be array of strings', (item) => {
147-
expect(Array.isArray(item.commandBat)).toBe(true);
148-
item.commandBat.forEach(cmd => {
149-
expect(typeof cmd).toBe('string');
178+
test('all configurations with commandBat should have array of strings', () => {
179+
configurations.filter(item => item.commandBat).forEach((item) => {
180+
expect(Array.isArray(item.commandBat)).toBe(true);
181+
item.commandBat.forEach(cmd => {
182+
expect(typeof cmd).toBe('string');
183+
});
150184
});
151185
});
152186

153-
test.each(configurations.filter(item => item.warning))('configuration "$name" warning should be a string', (item) => {
154-
expect(typeof item.warning).toBe('string');
155-
expect(item.warning.length).toBeGreaterThan(0);
187+
test('all configurations with warning should have string warning', () => {
188+
configurations.filter(item => item.warning).forEach((item) => {
189+
expect(typeof item.warning).toBe('string');
190+
expect(item.warning.length).toBeGreaterThan(0);
191+
});
156192
});
157193
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Test helper for loading configurations in Jest environment
3+
* Uses Node.js glob instead of Vite's import.meta.glob
4+
*/
5+
6+
import { glob } from 'glob';
7+
import path from 'path';
8+
import { fileURLToPath } from 'url';
9+
10+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
11+
const configurationsPath = path.join(__dirname, '../../src/data/configurations');
12+
13+
/**
14+
* Load all configuration entries from the configurations directory
15+
* @returns {Promise<Array>} Array of all configuration objects
16+
*/
17+
export const loadTestConfigurations = async () => {
18+
const files = await glob('**/*.js', { cwd: configurationsPath });
19+
const items = [];
20+
21+
for (const file of files) {
22+
const fullPath = path.join(configurationsPath, file);
23+
const module = await import(fullPath);
24+
if (module.default) {
25+
items.push(module.default);
26+
}
27+
}
28+
29+
return items.sort((a, b) => a.name.localeCompare(b.name));
30+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Test helper for loading software catalog in Jest environment
3+
* Uses Node.js glob instead of Vite's import.meta.glob
4+
*/
5+
6+
import { glob } from 'glob';
7+
import path from 'path';
8+
import { fileURLToPath } from 'url';
9+
10+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
11+
const softwarePath = path.join(__dirname, '../../src/data/software');
12+
13+
/**
14+
* Load all software entries from the software directory
15+
* @returns {Promise<Array>} Array of all software objects
16+
*/
17+
export const loadTestSoftwareCatalog = async () => {
18+
const files = await glob('**/*.js', { cwd: softwarePath });
19+
const items = [];
20+
21+
for (const file of files) {
22+
const fullPath = path.join(softwarePath, file);
23+
const module = await import(fullPath);
24+
if (module.default) {
25+
items.push(module.default);
26+
}
27+
}
28+
29+
return items.sort((a, b) => a.name.localeCompare(b.name));
30+
};

0 commit comments

Comments
 (0)