@justinmoto/frontend-guardian-core 0.1.4 → 0.1.6
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/dist/scanEngine.d.ts.map +1 -1
- package/dist/scanEngine.js +73 -11
- package/package.json +10 -7
package/dist/scanEngine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanEngine.d.ts","sourceRoot":"","sources":["../src/scanEngine.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AACF,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAClF,MAAM,MAAM,UAAU,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,UAAU,EAAE,SAAS,EAAE,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"scanEngine.d.ts","sourceRoot":"","sources":["../src/scanEngine.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AACF,MAAM,MAAM,SAAS,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAClF,MAAM,MAAM,UAAU,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAC;IAAC,UAAU,EAAE,SAAS,EAAE,CAAA;CAAE,CAAC;AA2W/G,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAajE;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,UAAU,CAG7E"}
|
package/dist/scanEngine.js
CHANGED
|
@@ -142,16 +142,30 @@ function getUnusedVariablesWithLine(ast) {
|
|
|
142
142
|
});
|
|
143
143
|
return unused;
|
|
144
144
|
}
|
|
145
|
+
function isCardLike(className) {
|
|
146
|
+
const hasRounded = /\brounded[\w-]*/.test(className);
|
|
147
|
+
const hasBg = /\bbg-/.test(className) || /\bbackdrop-/.test(className);
|
|
148
|
+
return !!(hasRounded && hasBg);
|
|
149
|
+
}
|
|
145
150
|
function runAnalysis(fileContents) {
|
|
146
151
|
const sourceOnly = fileContents.filter((f) => isSourcePath(f.path));
|
|
147
152
|
const warnings = [];
|
|
148
153
|
const jsxHashes = new Map();
|
|
149
|
-
const
|
|
154
|
+
const spacingByContext = new Map();
|
|
150
155
|
const radiusByFile = new Map();
|
|
151
156
|
const colorClassesByFile = new Map();
|
|
152
157
|
const arbitraryColorByFile = new Map();
|
|
158
|
+
const hexUsedInPlaces = new Map();
|
|
159
|
+
const pixelUsageLocations = [];
|
|
153
160
|
const buttonDataByFile = new Map();
|
|
161
|
+
const PX_ARBITRARY = /\[\d+(\.\d+)?\s*px\]/;
|
|
162
|
+
const BORDER_PX_ONLY = /^border-\[\d+(\.\d+)?\s*px\]$/;
|
|
154
163
|
const MAX_LOCATIONS = 25;
|
|
164
|
+
function getSpacingMap(context) {
|
|
165
|
+
if (!spacingByContext.has(context))
|
|
166
|
+
spacingByContext.set(context, new Map());
|
|
167
|
+
return spacingByContext.get(context);
|
|
168
|
+
}
|
|
155
169
|
for (const { path: filePath, code } of sourceOnly) {
|
|
156
170
|
try {
|
|
157
171
|
const ast = parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"], attachComment: false });
|
|
@@ -175,6 +189,8 @@ function runAnalysis(fileContents) {
|
|
|
175
189
|
}
|
|
176
190
|
const classEntries = extractClassNamesWithLines(ast);
|
|
177
191
|
for (const { value: cn, line } of classEntries) {
|
|
192
|
+
const context = isCardLike(cn) ? "card" : "default";
|
|
193
|
+
const spacingData = getSpacingMap(context);
|
|
178
194
|
const parts = cn.split(/\s+/).filter(Boolean);
|
|
179
195
|
for (const p of parts) {
|
|
180
196
|
if (/^p[xy]?-[a-z0-9]+$/.test(p) || /^m[xy]?-[a-z0-9]+$/.test(p)) {
|
|
@@ -195,8 +211,26 @@ function runAnalysis(fileContents) {
|
|
|
195
211
|
if (!colorClassesByFile.has(filePath))
|
|
196
212
|
colorClassesByFile.set(filePath, new Set());
|
|
197
213
|
colorClassesByFile.get(filePath).add(p);
|
|
198
|
-
|
|
214
|
+
const colorPart = p.replace(/^(bg|text|border)-/, "");
|
|
215
|
+
if (/\[#|\[rgb|\[hsl/.test(colorPart)) {
|
|
199
216
|
arbitraryColorByFile.set(filePath, line);
|
|
217
|
+
const hexMatch = colorPart.match(/\[#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\]/);
|
|
218
|
+
if (hexMatch) {
|
|
219
|
+
let hex = hexMatch[1].toLowerCase();
|
|
220
|
+
if (hex.length === 3)
|
|
221
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
222
|
+
const key = "#" + hex;
|
|
223
|
+
if (!hexUsedInPlaces.has(key))
|
|
224
|
+
hexUsedInPlaces.set(key, []);
|
|
225
|
+
const locs = hexUsedInPlaces.get(key);
|
|
226
|
+
if (locs.length < MAX_LOCATIONS)
|
|
227
|
+
locs.push({ file: filePath, line });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (PX_ARBITRARY.test(p) && !BORDER_PX_ONLY.test(p)) {
|
|
232
|
+
if (pixelUsageLocations.length < MAX_LOCATIONS)
|
|
233
|
+
pixelUsageLocations.push({ file: filePath, line });
|
|
200
234
|
}
|
|
201
235
|
}
|
|
202
236
|
}
|
|
@@ -219,14 +253,19 @@ function runAnalysis(fileContents) {
|
|
|
219
253
|
});
|
|
220
254
|
}
|
|
221
255
|
}
|
|
222
|
-
for (const [,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
256
|
+
for (const [context, spacingData] of spacingByContext) {
|
|
257
|
+
for (const [, data] of spacingData) {
|
|
258
|
+
if (data.values.size > 1) {
|
|
259
|
+
const label = context === "card" ? " (cards/boxes)" : "";
|
|
260
|
+
warnings.push({
|
|
261
|
+
file: "(project)",
|
|
262
|
+
locations: data.locations.slice(0, MAX_LOCATIONS),
|
|
263
|
+
message: `Mixed spacing values${label}: ${[...data.values].join(", ")}`,
|
|
264
|
+
suggestion: context === "card"
|
|
265
|
+
? "Use one padding for all cards/boxes (e.g. p-6)."
|
|
266
|
+
: "Pick one spacing scale for containers/layout (e.g. p-4) and use it consistently.",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
230
269
|
}
|
|
231
270
|
}
|
|
232
271
|
const radiusFileList = [...radiusByFile.entries()];
|
|
@@ -251,6 +290,28 @@ function runAnalysis(fileContents) {
|
|
|
251
290
|
suggestion: "Prefer Tailwind palette classes (e.g. bg-zinc-100, text-emerald-600) for consistent theming and maintenance.",
|
|
252
291
|
});
|
|
253
292
|
}
|
|
293
|
+
const repeatedHexes = [...hexUsedInPlaces.entries()].filter(([, locs]) => locs.length >= 2);
|
|
294
|
+
if (repeatedHexes.length > 0) {
|
|
295
|
+
const hexList = repeatedHexes.map(([hex]) => `${hex} (${hexUsedInPlaces.get(hex).length} places)`).join(", ");
|
|
296
|
+
const firstLoc = repeatedHexes[0][1][0];
|
|
297
|
+
const allLocs = repeatedHexes.flatMap(([, locs]) => locs).slice(0, MAX_LOCATIONS);
|
|
298
|
+
warnings.push({
|
|
299
|
+
file: firstLoc.file,
|
|
300
|
+
line: firstLoc.line,
|
|
301
|
+
locations: allLocs,
|
|
302
|
+
message: `Repeated hex color(s) used in multiple places: ${hexList}.`,
|
|
303
|
+
suggestion: "Add to global CSS (e.g. globals.css or index.css) as a custom utility (e.g. .text-navy) or CSS variable, or extend Tailwind theme in tailwind.config so you define it once and reuse (e.g. text-navy) instead of repeating the hex.",
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (pixelUsageLocations.length > 0) {
|
|
307
|
+
warnings.push({
|
|
308
|
+
file: pixelUsageLocations[0].file,
|
|
309
|
+
line: pixelUsageLocations[0].line,
|
|
310
|
+
locations: pixelUsageLocations.slice(0, MAX_LOCATIONS),
|
|
311
|
+
message: "Pixel (px) used for sizing or spacing (e.g. w-[10px], p-[4px], text-[14px]).",
|
|
312
|
+
suggestion: "Prefer em and rem over px because they are responsive and scalable. The design adjusts more easily to screen size and user settings. Reserve px for static values like border width (e.g. border-[1px]).",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
254
315
|
const buttonPadding = new Map();
|
|
255
316
|
const buttonRadius = new Map();
|
|
256
317
|
for (const [filePath, entries] of buttonDataByFile) {
|
|
@@ -310,7 +371,8 @@ function runAnalysis(fileContents) {
|
|
|
310
371
|
if (files.length > 1)
|
|
311
372
|
duplicates.push({ files, reason: dupReason, suggestion: dupSuggestion });
|
|
312
373
|
}
|
|
313
|
-
const
|
|
374
|
+
const warningsCounted = warnings.filter((w) => !w.message.startsWith("Pixel (px) used"));
|
|
375
|
+
const penalty = warningsCounted.length * 5 + duplicates.length * 10;
|
|
314
376
|
const score = Math.max(0, Math.min(100, 100 - penalty));
|
|
315
377
|
return { score, filesScanned: sourceOnly.length, warnings, duplicates };
|
|
316
378
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justinmoto/frontend-guardian-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Scan engine for Frontend Guardian. To run scans from the CLI use: npx frontend-guardian .",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
"files": [
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@babel/parser": "^7.26.0",
|
|
21
21
|
"@babel/traverse": "^7.26.0",
|
|
@@ -35,5 +35,8 @@
|
|
|
35
35
|
"components",
|
|
36
36
|
"ast"
|
|
37
37
|
],
|
|
38
|
-
"license": "MIT"
|
|
39
|
-
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc"
|
|
41
|
+
}
|
|
42
|
+
}
|