@svgicons-com/cli 0.1.0-alpha.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 +281 -0
- package/RELEASE_NOTES.md +42 -0
- package/bin/svgicons.js +13 -0
- package/package.json +26 -0
- package/src/api.js +194 -0
- package/src/cli.js +1900 -0
- package/src/config.js +134 -0
- package/src/downloads.js +173 -0
- package/src/errors.js +127 -0
- package/src/format.js +121 -0
- package/src/project.js +270 -0
- package/src/redact.js +43 -0
- package/src/scanner.js +247 -0
package/src/project.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { sanitizeFileSegment } from './downloads.js';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_MANIFEST = 'svgicons.json';
|
|
7
|
+
export const DEFAULT_LOCKFILE = 'svgicons.lock';
|
|
8
|
+
export const DEFAULT_OUTPUT_DIR = 'svgicons-icons';
|
|
9
|
+
|
|
10
|
+
export function manifestPath(options = {}, cwd = process.cwd()) {
|
|
11
|
+
return resolve(cwd, String(options.manifest || DEFAULT_MANIFEST));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function lockfilePath(options = {}, cwd = process.cwd()) {
|
|
15
|
+
return resolve(cwd, String(options.lockfile || DEFAULT_LOCKFILE));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createManifest(options = {}) {
|
|
19
|
+
const collection = options.collection && options.collection !== true
|
|
20
|
+
? String(options.collection)
|
|
21
|
+
: '';
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
version: 1,
|
|
25
|
+
framework: String(options.framework || 'svg'),
|
|
26
|
+
format: String(options.format || 'svg'),
|
|
27
|
+
output: String(options.output || DEFAULT_OUTPUT_DIR),
|
|
28
|
+
icons: [],
|
|
29
|
+
collections: collection ? [{ ref: collection }] : [],
|
|
30
|
+
licensePolicy: {
|
|
31
|
+
allow: [],
|
|
32
|
+
deny: [],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function readJsonFile(path) {
|
|
38
|
+
const raw = await readFile(path, 'utf8');
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function writeJsonFile(path, value, { force = true } = {}) {
|
|
43
|
+
await mkdir(dirname(path), { recursive: true });
|
|
44
|
+
await writeFile(path, `${stableStringify(value)}\n`, {
|
|
45
|
+
flag: force ? 'w' : 'wx',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function writeTextFile(path, value) {
|
|
50
|
+
await mkdir(dirname(path), { recursive: true });
|
|
51
|
+
await writeFile(path, value.endsWith('\n') ? value : `${value}\n`, 'utf8');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeManifest(manifest) {
|
|
55
|
+
return {
|
|
56
|
+
version: 1,
|
|
57
|
+
framework: manifest.framework || 'svg',
|
|
58
|
+
format: manifest.format || 'svg',
|
|
59
|
+
output: manifest.output || DEFAULT_OUTPUT_DIR,
|
|
60
|
+
icons: Array.isArray(manifest.icons) ? manifest.icons : [],
|
|
61
|
+
collections: Array.isArray(manifest.collections) ? manifest.collections : [],
|
|
62
|
+
licensePolicy: {
|
|
63
|
+
allow: Array.isArray(manifest.licensePolicy?.allow) ? manifest.licensePolicy.allow : [],
|
|
64
|
+
deny: Array.isArray(manifest.licensePolicy?.deny) ? manifest.licensePolicy.deny : [],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeIconManifestEntry(entry) {
|
|
70
|
+
if (typeof entry === 'string' || typeof entry === 'number') {
|
|
71
|
+
return { ref: String(entry) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
ref: String(entry?.ref || entry?.id || '').trim(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function normalizeCollectionManifestEntry(entry) {
|
|
80
|
+
if (typeof entry === 'string' || typeof entry === 'number') {
|
|
81
|
+
return { ref: String(entry) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
ref: String(entry?.ref || entry?.id || entry?.slug || entry?.name || '').trim(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function lockIconFromIcon(icon) {
|
|
90
|
+
const svg = icon.svg || icon.body || '';
|
|
91
|
+
const iconSet = icon.iconSet || {};
|
|
92
|
+
const ref = `${Number(icon.id)}-${sanitizeFileSegment(icon.name || 'icon')}`;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
id: Number(icon.id),
|
|
96
|
+
name: String(icon.name || ''),
|
|
97
|
+
ref,
|
|
98
|
+
width: Number(icon.width || 0) || null,
|
|
99
|
+
height: Number(icon.height || 0) || null,
|
|
100
|
+
pageUrl: icon.pageUrl || null,
|
|
101
|
+
svgHash: svg ? `sha256-${sha256(svg)}` : null,
|
|
102
|
+
license: iconSet.license || 'Unknown license',
|
|
103
|
+
licenseUrl: iconSet.licenseUrl || iconSet.license_url || null,
|
|
104
|
+
iconSet: {
|
|
105
|
+
id: iconSet.id || icon.icon_set_id || null,
|
|
106
|
+
name: iconSet.name || null,
|
|
107
|
+
prefix: iconSet.prefix || null,
|
|
108
|
+
},
|
|
109
|
+
svg,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createLockfile(icons, manifest = {}) {
|
|
114
|
+
const deduped = new Map();
|
|
115
|
+
|
|
116
|
+
for (const icon of icons) {
|
|
117
|
+
const locked = lockIconFromIcon(icon);
|
|
118
|
+
if (locked.id) {
|
|
119
|
+
deduped.set(locked.id, locked);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
version: 1,
|
|
125
|
+
source: {
|
|
126
|
+
manifestVersion: manifest.version || 1,
|
|
127
|
+
framework: manifest.framework || 'svg',
|
|
128
|
+
format: manifest.format || 'svg',
|
|
129
|
+
output: manifest.output || DEFAULT_OUTPUT_DIR,
|
|
130
|
+
},
|
|
131
|
+
icons: [...deduped.values()].sort((a, b) => a.id - b.id),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function svgFilenameForIcon(icon) {
|
|
136
|
+
return `${icon.ref || `${icon.id}-${sanitizeFileSegment(icon.name || 'icon')}`}.svg`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildLicenseReport(lockfile, policy = {}) {
|
|
140
|
+
const allow = normalizeLicenseList(policy.allow);
|
|
141
|
+
const deny = normalizeLicenseList(policy.deny);
|
|
142
|
+
const licenses = new Map();
|
|
143
|
+
const violations = [];
|
|
144
|
+
|
|
145
|
+
for (const icon of lockfile.icons || []) {
|
|
146
|
+
const license = icon.license || 'Unknown license';
|
|
147
|
+
const normalized = normalizeLicense(license);
|
|
148
|
+
const current = licenses.get(normalized) || {
|
|
149
|
+
license,
|
|
150
|
+
count: 0,
|
|
151
|
+
icons: [],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
current.count += 1;
|
|
155
|
+
current.icons.push({
|
|
156
|
+
id: icon.id,
|
|
157
|
+
name: icon.name,
|
|
158
|
+
ref: icon.ref,
|
|
159
|
+
iconSet: icon.iconSet?.name || icon.iconSet?.prefix || '',
|
|
160
|
+
});
|
|
161
|
+
licenses.set(normalized, current);
|
|
162
|
+
|
|
163
|
+
if (deny.has(normalized)) {
|
|
164
|
+
violations.push({ reason: 'denied', license, icon });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (allow.size > 0 && !allow.has(normalized)) {
|
|
169
|
+
violations.push({ reason: 'not_allowed', license, icon });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
ok: violations.length === 0,
|
|
175
|
+
checkedIcons: (lockfile.icons || []).length,
|
|
176
|
+
licenses: [...licenses.values()].sort((a, b) => a.license.localeCompare(b.license)),
|
|
177
|
+
violations: violations.map(({ reason, license, icon }) => ({
|
|
178
|
+
reason,
|
|
179
|
+
license,
|
|
180
|
+
icon: {
|
|
181
|
+
id: icon.id,
|
|
182
|
+
name: icon.name,
|
|
183
|
+
ref: icon.ref,
|
|
184
|
+
iconSet: icon.iconSet,
|
|
185
|
+
},
|
|
186
|
+
})),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function licenseReportToMarkdown(report) {
|
|
191
|
+
const lines = [
|
|
192
|
+
'# Third-party SVG icons',
|
|
193
|
+
'',
|
|
194
|
+
`Checked icons: ${report.checkedIcons}`,
|
|
195
|
+
'',
|
|
196
|
+
'## Licenses',
|
|
197
|
+
'',
|
|
198
|
+
'| License | Icons |',
|
|
199
|
+
'| --- | ---: |',
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
for (const item of report.licenses) {
|
|
203
|
+
lines.push(`| ${escapeMarkdownCell(item.license)} | ${item.count} |`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (report.violations.length > 0) {
|
|
207
|
+
lines.push('', '## Policy violations', '');
|
|
208
|
+
for (const violation of report.violations) {
|
|
209
|
+
lines.push(`- ${violation.icon.ref}: ${violation.license} (${violation.reason})`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return `${lines.join('\n')}\n`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function licenseReportToCsv(report) {
|
|
217
|
+
const rows = [['license', 'icons']];
|
|
218
|
+
for (const item of report.licenses) {
|
|
219
|
+
rows.push([item.license, String(item.count)]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return `${rows.map((row) => row.map(csvCell).join(',')).join('\n')}\n`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function stableStringify(value) {
|
|
226
|
+
return JSON.stringify(sortForJson(value), null, 2);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function sha256(value) {
|
|
230
|
+
return createHash('sha256').update(String(value)).digest('hex');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function sortForJson(value) {
|
|
234
|
+
if (Array.isArray(value)) {
|
|
235
|
+
return value.map(sortForJson);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!value || typeof value !== 'object') {
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return Object.keys(value)
|
|
243
|
+
.sort()
|
|
244
|
+
.reduce((result, key) => {
|
|
245
|
+
result[key] = sortForJson(value[key]);
|
|
246
|
+
return result;
|
|
247
|
+
}, {});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function normalizeLicenseList(values = []) {
|
|
251
|
+
return new Set(
|
|
252
|
+
values
|
|
253
|
+
.flatMap((value) => String(value || '').split(','))
|
|
254
|
+
.map((value) => normalizeLicense(value))
|
|
255
|
+
.filter(Boolean),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function normalizeLicense(value) {
|
|
260
|
+
return String(value || '').trim().toLowerCase();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function escapeMarkdownCell(value) {
|
|
264
|
+
return String(value || '').replace(/\|/g, '\\|');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function csvCell(value) {
|
|
268
|
+
const text = String(value ?? '');
|
|
269
|
+
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
270
|
+
}
|
package/src/redact.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const SECRET_KEY_PATTERN = /(token|secret|password|authorization|api[_-]?key|bearer)/i;
|
|
2
|
+
const BEARER_PATTERN = /Bearer\s+[A-Za-z0-9._~+/=-]+/gi;
|
|
3
|
+
const TOKEN_LIKE_PATTERN = /\b(?:sk|pk|whsec|svgicons)_[A-Za-z0-9._~+/=-]{12,}\b/g;
|
|
4
|
+
|
|
5
|
+
export const REDACTED = '[redacted]';
|
|
6
|
+
|
|
7
|
+
export function redact(value) {
|
|
8
|
+
if (value === null || value === undefined) {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof value === 'string') {
|
|
13
|
+
return redactString(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value.map((item) => redact(item));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof value === 'object') {
|
|
21
|
+
return redactObject(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function redactObject(value) {
|
|
28
|
+
const output = {};
|
|
29
|
+
|
|
30
|
+
for (const [key, item] of Object.entries(value || {})) {
|
|
31
|
+
output[key] = SECRET_KEY_PATTERN.test(key) && item
|
|
32
|
+
? REDACTED
|
|
33
|
+
: redact(item);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return output;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function redactString(value) {
|
|
40
|
+
return String(value)
|
|
41
|
+
.replace(BEARER_PATTERN, `Bearer ${REDACTED}`)
|
|
42
|
+
.replace(TOKEN_LIKE_PATTERN, REDACTED);
|
|
43
|
+
}
|
package/src/scanner.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { opendir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename, extname, join, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_EXTENSIONS = new Set([
|
|
5
|
+
'.astro',
|
|
6
|
+
'.blade.php',
|
|
7
|
+
'.css',
|
|
8
|
+
'.html',
|
|
9
|
+
'.js',
|
|
10
|
+
'.jsx',
|
|
11
|
+
'.md',
|
|
12
|
+
'.scss',
|
|
13
|
+
'.svelte',
|
|
14
|
+
'.ts',
|
|
15
|
+
'.tsx',
|
|
16
|
+
'.vue',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const EXCLUDED_DIRS = new Set([
|
|
20
|
+
'.git',
|
|
21
|
+
'.next',
|
|
22
|
+
'.nuxt',
|
|
23
|
+
'.output',
|
|
24
|
+
'.turbo',
|
|
25
|
+
'build',
|
|
26
|
+
'coverage',
|
|
27
|
+
'dist',
|
|
28
|
+
'node_modules',
|
|
29
|
+
'public/build',
|
|
30
|
+
'storage',
|
|
31
|
+
'vendor',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const CONCEPT_PATTERNS = {
|
|
35
|
+
account: ['account', 'profile', 'avatar'],
|
|
36
|
+
analytics: ['analytics', 'chart', 'graph', 'metric', 'report'],
|
|
37
|
+
auth: ['auth', 'login', 'logout', 'password', 'sign in', 'signin'],
|
|
38
|
+
billing: ['billing', 'invoice', 'payment', 'pricing', 'subscription'],
|
|
39
|
+
calendar: ['calendar', 'date', 'schedule'],
|
|
40
|
+
check: ['check', 'success', 'complete', 'done'],
|
|
41
|
+
close: ['close', 'cancel', 'dismiss', 'remove'],
|
|
42
|
+
dashboard: ['dashboard', 'admin', 'overview'],
|
|
43
|
+
download: ['download', 'export'],
|
|
44
|
+
edit: ['edit', 'pencil', 'compose'],
|
|
45
|
+
file: ['file', 'document', 'attachment'],
|
|
46
|
+
home: ['home', 'landing'],
|
|
47
|
+
lock: ['lock', 'security', 'permission', 'private'],
|
|
48
|
+
mail: ['mail', 'email', 'inbox'],
|
|
49
|
+
menu: ['menu', 'navigation', 'navbar', 'sidebar'],
|
|
50
|
+
notification: ['notification', 'alert', 'bell', 'warning'],
|
|
51
|
+
search: ['search', 'filter', 'find'],
|
|
52
|
+
settings: ['settings', 'preferences', 'config', 'gear'],
|
|
53
|
+
trash: ['trash', 'delete', 'remove'],
|
|
54
|
+
upload: ['upload', 'import'],
|
|
55
|
+
user: ['user', 'users', 'member', 'team'],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export async function scanProject(inputPath = '.', options = {}) {
|
|
59
|
+
const root = resolve(inputPath);
|
|
60
|
+
const maxFiles = Number(options.maxFiles || 500);
|
|
61
|
+
const files = [];
|
|
62
|
+
|
|
63
|
+
await collectFiles(root, files, {
|
|
64
|
+
maxFiles,
|
|
65
|
+
extensions: options.extensions || DEFAULT_EXTENSIONS,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const concepts = new Map();
|
|
69
|
+
const iconIds = new Set();
|
|
70
|
+
const iconRefs = new Set();
|
|
71
|
+
const iconSetPrefixes = new Set();
|
|
72
|
+
const generatedIconRefs = new Set();
|
|
73
|
+
let bytesRead = 0;
|
|
74
|
+
let generatedImportCount = 0;
|
|
75
|
+
let inlineSvgCount = 0;
|
|
76
|
+
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const text = await readFile(file, 'utf8').catch(() => '');
|
|
79
|
+
bytesRead += Buffer.byteLength(text, 'utf8');
|
|
80
|
+
detectConcepts(`${file}\n${text}`, concepts);
|
|
81
|
+
detectSvgiconsUrls(text, iconIds, iconRefs, iconSetPrefixes);
|
|
82
|
+
|
|
83
|
+
const generated = detectGeneratedSvgImports(text);
|
|
84
|
+
generatedImportCount += generated.count;
|
|
85
|
+
for (const ref of generated.refs) {
|
|
86
|
+
generatedIconRefs.add(ref);
|
|
87
|
+
iconIds.add(ref.split('-')[0]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
inlineSvgCount += detectInlineSvgCount(text);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sortedConcepts = [...concepts.entries()]
|
|
94
|
+
.map(([name, count]) => ({ name, count }))
|
|
95
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
96
|
+
|
|
97
|
+
const projectName = titleFromRoot(root);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
root,
|
|
101
|
+
collection: {
|
|
102
|
+
name: options.collectionName || `${projectName} icons`,
|
|
103
|
+
description: `Icon candidates detected from ${projectName} codebase scan.`,
|
|
104
|
+
},
|
|
105
|
+
concepts: sortedConcepts,
|
|
106
|
+
existingIconIds: [...iconIds].sort((a, b) => Number(a) - Number(b)),
|
|
107
|
+
existingIconRefs: [...iconRefs].sort(sortRefsById),
|
|
108
|
+
generatedIconRefs: [...generatedIconRefs].sort(sortRefsById),
|
|
109
|
+
iconSetPrefixes: [...iconSetPrefixes].sort(),
|
|
110
|
+
stats: {
|
|
111
|
+
filesScanned: files.length,
|
|
112
|
+
maxFiles,
|
|
113
|
+
bytesRead,
|
|
114
|
+
generatedImportCount,
|
|
115
|
+
inlineSvgCount,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function collectFiles(directory, files, options) {
|
|
121
|
+
if (files.length >= options.maxFiles) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = await opendir(directory);
|
|
128
|
+
} catch {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for await (const entry of entries) {
|
|
133
|
+
if (files.length >= options.maxFiles) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const path = join(directory, entry.name);
|
|
138
|
+
|
|
139
|
+
if (entry.isDirectory()) {
|
|
140
|
+
if (isExcludedDirectory(path)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await collectFiles(path, files, options);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!entry.isFile()) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!isSupportedFile(path, options.extensions)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const info = await stat(path).catch(() => null);
|
|
157
|
+
if (!info || info.size > 256 * 1024) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
files.push(path);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isSupportedFile(path, extensions) {
|
|
166
|
+
if (path.endsWith('.blade.php')) {
|
|
167
|
+
return extensions.has('.blade.php');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return extensions.has(extname(path).toLowerCase());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isExcludedDirectory(path) {
|
|
174
|
+
const normalized = path.split(sep).join('/');
|
|
175
|
+
|
|
176
|
+
return [...EXCLUDED_DIRS].some((dir) => normalized.endsWith(`/${dir}`) || normalized.includes(`/${dir}/`));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function detectConcepts(text, concepts) {
|
|
180
|
+
const normalized = text.toLowerCase().replace(/[-_]/g, ' ');
|
|
181
|
+
|
|
182
|
+
for (const [concept, needles] of Object.entries(CONCEPT_PATTERNS)) {
|
|
183
|
+
let count = 0;
|
|
184
|
+
|
|
185
|
+
for (const needle of needles) {
|
|
186
|
+
const pattern = new RegExp(`\\b${escapeRegExp(needle)}\\b`, 'gi');
|
|
187
|
+
count += normalized.match(pattern)?.length || 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (count > 0) {
|
|
191
|
+
concepts.set(concept, (concepts.get(concept) || 0) + count);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function detectSvgiconsUrls(text, iconIds, iconRefs, iconSetPrefixes) {
|
|
197
|
+
for (const match of text.matchAll(/(?:svgicons\.com)?\/icon\/(\d+)\/([a-z0-9._-]+)/gi)) {
|
|
198
|
+
const id = match[1];
|
|
199
|
+
const slug = normalizeSlug(match[2]);
|
|
200
|
+
iconIds.add(id);
|
|
201
|
+
iconRefs.add(`${id}-${slug}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const match of text.matchAll(/svgicons\.com\/icon-set\/([a-z0-9-]+)|\/icon-set\/([a-z0-9-]+)/gi)) {
|
|
205
|
+
iconSetPrefixes.add(match[1] || match[2]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function detectGeneratedSvgImports(text) {
|
|
210
|
+
const refs = new Set();
|
|
211
|
+
let count = 0;
|
|
212
|
+
|
|
213
|
+
for (const match of text.matchAll(/(?:import\s+[^'"]+\s+from\s+|require\()\s*['"][^'"]*?(\d+)-([a-z0-9._-]+)\.svg['"]/gi)) {
|
|
214
|
+
count++;
|
|
215
|
+
refs.add(`${match[1]}-${normalizeSlug(match[2])}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { count, refs };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function detectInlineSvgCount(text) {
|
|
222
|
+
return [...text.matchAll(/<svg(?:\s|>)/gi)].length;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function titleFromRoot(root) {
|
|
226
|
+
return basename(root).replace(/[-_]+/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function normalizeSlug(value) {
|
|
230
|
+
return String(value || '')
|
|
231
|
+
.trim()
|
|
232
|
+
.toLowerCase()
|
|
233
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
234
|
+
.replace(/-+/g, '-')
|
|
235
|
+
.replace(/^[._-]+|[._-]+$/g, '');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function sortRefsById(a, b) {
|
|
239
|
+
const aId = Number(String(a).split('-')[0]);
|
|
240
|
+
const bId = Number(String(b).split('-')[0]);
|
|
241
|
+
|
|
242
|
+
return aId - bId || String(a).localeCompare(String(b));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function escapeRegExp(value) {
|
|
246
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
247
|
+
}
|