@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.
@@ -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;AA4S/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"}
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"}
@@ -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 spacingData = new Map();
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
- if (/\[#|\[rgb|\[hsl/.test(p.replace(/^(bg|text|border)-/, "")))
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 [, data] of spacingData) {
223
- if (data.values.size > 1) {
224
- warnings.push({
225
- file: "(project)",
226
- locations: data.locations.slice(0, MAX_LOCATIONS),
227
- message: `Mixed spacing values: ${[...data.values].join(", ")}`,
228
- suggestion: "Pick one spacing scale (e.g. px-4, py-2) for similar elements and use it across the project so spacing stays consistent.",
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 penalty = warnings.length * 5 + duplicates.length * 10;
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.4",
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": ["dist", "README.md"],
16
- "scripts": {
17
- "build": "tsc"
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
+ }