vtex-css-sanitizer-cli 1.0.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 ADDED
@@ -0,0 +1,111 @@
1
+ # VTEX CSS Sanitizer
2
+
3
+ ---
4
+
5
+ [![npm version](https://badge.fury.io/js/vtex-deploy-helper.svg)](https://badge.fury.io/js/vtex-deploy-helper)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ---
9
+
10
+ vtex-css-sanitizer es una herramienta de línea de comandos (CLI) diseñada para limpiar y optimizar las hojas de estilo en proyectos de **VTEX IO**. Analiza tu base de código para encontrar clases CSS huérfanas y declaraciones `blockClass` sin uso, ayudándote a mantener tu proyecto limpio, performante y fácil de mantener.
11
+
12
+ ---
13
+
14
+ ### El Problema
15
+
16
+ En el desarrollo diario con VTEX IO, es común que:
17
+
18
+ 1. **Las reglas CSS queden huérfanas:** Se elimina un `blockClass` de un archivo JSON, pero sus estilos asociados permanecen en los archivos `.css`.
19
+ 2. **Las `blockClass` queden sin uso:** Se declara un `blockClass` en un bloque, pero nunca se crea una regla CSS para estilizarlo.
20
+
21
+ Estos restos de código aumentan el tamaño de los bundles y hacen que la base de código sea más difícil de navegar. Esta herramienta automatiza el proceso de detección y limpieza.
22
+
23
+ ### ✨ Características
24
+
25
+ - **Análisis Bidireccional:** Encuentra tanto CSS sin `blockClass` como `blockClass` sin CSS.
26
+ - **Limpieza Interactiva:** El comando `fix` te guía a través de cada regla candidata, dándote el control total para decidir qué se elimina y qué se conserva.
27
+ - **Inteligente:** Reconoce las clases de estado dinámicas de VTEX (ej. `--isActive`) y solo valida el `blockClass` principal.
28
+ - **Seguro:** Ignora automáticamente los archivos CSS de componentes React custom para evitar falsos positivos.
29
+ - **Informes Detallados:** Genera informes en formato Markdown de cada análisis y sesión de limpieza para un registro histórico.
30
+
31
+ ### 📦 Instalación
32
+
33
+ Para usar esta herramienta en cualquier proyecto de tu máquina, instálala globalmente:
34
+
35
+ ```bash
36
+ npm install -g vtex-css-sanitizer
37
+ ```
38
+
39
+ ### 🚀 Uso
40
+
41
+ Navega a la carpeta raíz de tu proyecto VTEX IO y ejecuta los siguientes comandos.
42
+
43
+ #### 1. Analizar el Proyecto (`analyze`)
44
+
45
+ Este comando escanea tu proyecto en modo de solo lectura y te muestra un informe en la consola, además de generar un archivo Markdown en la carpeta `.sanitizer-reports/`.
46
+
47
+ ```bash
48
+ vtex-css-sanitizer analyze .
49
+ ```
50
+
51
+ **Salida de ejemplo en consola:**
52
+
53
+ ```
54
+ --- INFORME DE RESULTADOS ---
55
+
56
+ 🔴 Se encontraron 3 SUFIJOS CSS que no corresponden a ninguna 'blockClass' declarada:
57
+
58
+ - Sufijo: --main-header-old
59
+ └─ Usado en: styles/css/vtex.flex-layout.css (selector: ".flexRow--main-header-old")
60
+
61
+ 🟡 Se encontraron 2 'blockClass' declaradas que NO se usan en ningún archivo CSS:
62
+
63
+ - blockClass: "promo-banner-temporary"
64
+ └─ Declarada en: store/blocks/home/home.jsonc (en el bloque: "rich-text#promo-banner")
65
+
66
+ --- ANÁLISIS COMPLETADO ---
67
+
68
+ 📄 Informe de análisis guardado en: .sanitizer-reports/analysis-report-2025-07-17.md
69
+ ```
70
+
71
+ #### 2. Limpiar el Proyecto (`fix`)
72
+
73
+ Este comando inicia un proceso interactivo que te guiará regla por regla para que decidas cuál eliminar.
74
+
75
+ ```bash
76
+ vtex-css-sanitizer fix .
77
+ ```
78
+
79
+ **Proceso interactivo de ejemplo:**
80
+
81
+ ```
82
+ [ Progreso: Archivo 24 de 59 ]
83
+ ------------------------------------------------------------------
84
+ Revisando Archivo: styles/css/vtex.breadcrumb.css
85
+ Candidato 1 de 1
86
+ ------------------------------------------------------------------
87
+ Se encontró la siguiente regla CSS que podría no estar en uso:
88
+
89
+ :global(.vtex-breadcrumb-1-x-link--2) {
90
+ font-weight: 900;
91
+ }
92
+ ? ¿Deseas eliminar esta regla CSS? › (Y/n)
93
+ ```
94
+
95
+ - **(Y)** para `Sí` (elimina la regla).
96
+ - **(n)** para `No` (conserva la regla).
97
+ - **Ctrl+C** para cancelar el proceso.
98
+
99
+ Al finalizar, se genera un informe detallado de las reglas eliminadas y conservadas en la carpeta `.sanitizer-reports/`.
100
+
101
+ ### 📄 Informes
102
+
103
+ Todos los informes generados se guardan en una nueva carpeta `.sanitizer-reports` en la raíz de tu proyecto. Esta carpeta debería ser añadida a tu `.gitignore`.
104
+
105
+ ### 🤝 Contribuciones
106
+
107
+ Las contribuciones, issues y peticiones de funcionalidades son bienvenidas.
108
+
109
+ ### 📜 Licencia
110
+
111
+ Distribuido bajo la Licencia MIT. Ver `LICENSE` para más información.
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.analyzeCommand = analyzeCommand;
7
+ const file_finder_1 = require("../utils/file-finder");
8
+ const json_processor_1 = require("../utils/json-processor");
9
+ const css_processor_1 = require("../utils/css-processor");
10
+ const report_generator_1 = require("../utils/report-generator");
11
+ const path_1 = __importDefault(require("path"));
12
+ async function analyzeCommand(projectPath) {
13
+ console.log(`\n🔍 Iniciando análisis en: ${path_1.default.resolve(projectPath)}`);
14
+ // 1. Encontrar todos los archivos relevantes
15
+ const jsonFiles = (0, file_finder_1.findJsonFiles)(projectPath);
16
+ const cssFiles = (0, file_finder_1.findCssFiles)(projectPath);
17
+ if (jsonFiles.length === 0 || cssFiles.length === 0) {
18
+ console.error('❌ Error: No se encontraron archivos de store o styles/css. Asegúrate de que la ruta es correcta.');
19
+ return;
20
+ }
21
+ console.log(` - Encontrados ${jsonFiles.length} archivos de bloques (.jsonc, .json)`);
22
+ console.log(` - Encontrados ${cssFiles.length} archivos de estilos (.css)`);
23
+ // 2. Extraer la información
24
+ const declaredBlockClassesMap = await (0, json_processor_1.extractBlockClasses)(jsonFiles);
25
+ const usedCssSuffixesMap = await (0, css_processor_1.extractCssSuffixes)(cssFiles);
26
+ const declaredBlockClasses = new Set(declaredBlockClassesMap.keys());
27
+ const usedCssSuffixes = new Set(usedCssSuffixesMap.keys());
28
+ console.log(`\n📊 Análisis de datos:`);
29
+ console.log(` - ${declaredBlockClasses.size} 'blockClass' únicas declaradas.`);
30
+ console.log(` - ${usedCssSuffixes.size} sufijos de CSS únicos encontrados.`);
31
+ // 3. Comparar y encontrar discrepancias
32
+ // --- CSS sin uso ---
33
+ const unusedCss = [...usedCssSuffixes].filter(suffix => !declaredBlockClasses.has(suffix));
34
+ // --- blockClass sin uso ---
35
+ const unusedBlockClasses = [...declaredBlockClasses].filter(cls => !usedCssSuffixes.has(cls));
36
+ console.log('\n--- INFORME DE RESULTADOS ---');
37
+ if (unusedCss.length > 0) {
38
+ console.log(`\n🔴 Se encontraron ${unusedCss.length} SUFIJOS CSS que no corresponden a ninguna 'blockClass' declarada:`);
39
+ unusedCss.forEach(suffix => {
40
+ const locations = usedCssSuffixesMap.get(suffix) || [];
41
+ console.log(`\n - Sufijo: --${suffix}`);
42
+ locations.slice(0, 3).forEach(loc => {
43
+ console.log(` └─ Usado en: ${path_1.default.relative(projectPath, loc.filePath)} (selector: "${loc.selector}")`);
44
+ });
45
+ });
46
+ console.log("\n Estos estilos podrían ser eliminados. Usa el comando 'fix' para limpiarlos.");
47
+ }
48
+ else {
49
+ console.log(`\n✅ ¡Genial! Todos los sufijos CSS utilizados corresponden a una 'blockClass' declarada.`);
50
+ }
51
+ console.log('\n---------------------------------');
52
+ if (unusedBlockClasses.length > 0) {
53
+ console.log(`\n🟡 Se encontraron ${unusedBlockClasses.length} 'blockClass' declaradas que NO se usan en ningún archivo CSS:`);
54
+ unusedBlockClasses.forEach(cls => {
55
+ const locations = declaredBlockClassesMap.get(cls) || [];
56
+ console.log(`\n - blockClass: "${cls}"`);
57
+ locations.slice(0, 3).forEach(loc => {
58
+ console.log(` └─ Declarada en: ${path_1.default.relative(projectPath, loc.filePath)} (en el bloque: "${loc.blockName}")`);
59
+ });
60
+ });
61
+ console.log("\n Estas declaraciones son inútiles y pueden ser eliminadas de los archivos .jsonc/.json.");
62
+ }
63
+ else {
64
+ console.log(`\n✅ ¡Excelente! Todas las 'blockClass' declaradas tienen reglas CSS asociadas.`);
65
+ }
66
+ console.log('\n--- ANÁLISIS COMPLETADO ---\n');
67
+ try {
68
+ const reportPath = await (0, report_generator_1.generateAnalysisReport)(projectPath, unusedCss, unusedBlockClasses, usedCssSuffixesMap, declaredBlockClassesMap);
69
+ console.log(`\n📄 Informe de análisis guardado en: ${path_1.default.relative(projectPath, reportPath)}`);
70
+ }
71
+ catch (error) {
72
+ console.error('\n❌ Ocurrió un error al guardar el informe de análisis:', error);
73
+ }
74
+ }
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.fixCommand = fixCommand;
7
+ const file_finder_1 = require("../utils/file-finder");
8
+ const json_processor_1 = require("../utils/json-processor");
9
+ const css_processor_1 = require("../utils/css-processor");
10
+ const path_1 = __importDefault(require("path"));
11
+ const promises_1 = __importDefault(require("fs/promises"));
12
+ const postcss_1 = __importDefault(require("postcss"));
13
+ const prompts_1 = __importDefault(require("prompts"));
14
+ const report_generator_1 = require("../utils/report-generator");
15
+ async function fixCommand(projectPath) {
16
+ console.log(`\n🔧 Iniciando proceso de limpieza interactiva en: ${path_1.default.resolve(projectPath)}`);
17
+ // --- 1. Análisis (sin cambios) ---
18
+ const jsonFiles = (0, file_finder_1.findJsonFiles)(projectPath);
19
+ const cssFiles = (0, file_finder_1.findCssFiles)(projectPath);
20
+ const declaredBlockClassesMap = await (0, json_processor_1.extractBlockClasses)(jsonFiles);
21
+ const usedCssSuffixesMap = await (0, css_processor_1.extractCssSuffixes)(cssFiles);
22
+ const declaredBlockClasses = new Set(declaredBlockClassesMap.keys());
23
+ const usedCssSuffixes = new Set(usedCssSuffixesMap.keys());
24
+ const unusedSuffixes = new Set([...usedCssSuffixes].filter(suffix => !declaredBlockClasses.has(suffix)));
25
+ if (unusedSuffixes.size === 0) {
26
+ console.log('\n✅ ¡Genial! No se encontraron reglas CSS para eliminar. ¡El proyecto ya está limpio!');
27
+ return;
28
+ }
29
+ console.log(`\n🔎 Se han encontrado reglas CSS con sufijos potencialmente no utilizados. Vamos a revisarlas una por una.`);
30
+ console.log('------------------------------------------------------------------');
31
+ const deletedRules = [];
32
+ const keptRules = [];
33
+ let totalRulesRemoved = 0;
34
+ // --- 2. Procesamiento Interactivo con CONTADOR ---
35
+ const totalFiles = cssFiles.length;
36
+ for (let fileIndex = 0; fileIndex < totalFiles; fileIndex++) {
37
+ const filePath = cssFiles[fileIndex];
38
+ const originalContent = await promises_1.default.readFile(filePath, 'utf-8');
39
+ const root = postcss_1.default.parse(originalContent);
40
+ const candidates = (0, css_processor_1.identifyRulesForDeletion)(root, unusedSuffixes);
41
+ if (candidates.length === 0) {
42
+ continue;
43
+ }
44
+ let rulesRemovedInFile = 0;
45
+ for (let i = 0; i < candidates.length; i++) {
46
+ const rule = candidates[i];
47
+ const relativePath = path_1.default.relative(projectPath, filePath);
48
+ const ruleAsString = rule.toString();
49
+ console.clear();
50
+ // Se muestra el progreso general y el del archivo actual
51
+ console.log(`[ Progreso: Archivo ${fileIndex + 1} de ${totalFiles} ]`);
52
+ console.log(`------------------------------------------------------------------`);
53
+ console.log(`Revisando Archivo: ${relativePath}`);
54
+ console.log(`Candidato ${i + 1} de ${candidates.length}`);
55
+ console.log('------------------------------------------------------------------');
56
+ console.log('Se encontró la siguiente regla CSS que podría no estar en uso:');
57
+ console.log('\n\x1b[33m%s\x1b[0m', ruleAsString);
58
+ const response = await (0, prompts_1.default)({
59
+ type: 'confirm',
60
+ name: 'shouldDelete',
61
+ message: '¿Deseas eliminar esta regla CSS?',
62
+ initial: true
63
+ });
64
+ if (response.shouldDelete === undefined) {
65
+ console.log('\n🛑 Proceso de limpieza cancelado por el usuario.');
66
+ // Antes de salir, generamos el informe con lo que se haya hecho hasta ahora
67
+ if (deletedRules.length > 0 || keptRules.length > 0) {
68
+ await (0, report_generator_1.generateFixReport)(projectPath, deletedRules, keptRules);
69
+ console.log(`\n📄 Informe parcial de limpieza guardado.`);
70
+ }
71
+ return;
72
+ }
73
+ if (response.shouldDelete) {
74
+ rule.remove();
75
+ rulesRemovedInFile++;
76
+ deletedRules.push({ rule: ruleAsString, filePath });
77
+ console.log('\x1b[31m%s\x1b[0m', '🗑️ Regla eliminada.');
78
+ }
79
+ else {
80
+ keptRules.push({ rule: ruleAsString, filePath });
81
+ console.log('\x1b[32m%s\x1b[0m', '👍 Regla conservada.');
82
+ }
83
+ console.log('------------------------------------------------------------------');
84
+ }
85
+ if (rulesRemovedInFile > 0) {
86
+ const newContent = root.toString();
87
+ await promises_1.default.writeFile(filePath, newContent, 'utf-8');
88
+ console.log(`\n💾 Se guardaron los cambios en ${path_1.default.relative(projectPath, filePath)}. Se eliminaron ${rulesRemovedInFile} reglas.`);
89
+ totalRulesRemoved += rulesRemovedInFile;
90
+ await (0, prompts_1.default)({ type: 'invisible', name: 'continue', message: 'Presiona Enter para continuar con el siguiente archivo...' });
91
+ }
92
+ }
93
+ console.clear();
94
+ if (totalRulesRemoved > 0) {
95
+ console.log(`\n✅ Proceso completado. Se eliminaron un total de ${totalRulesRemoved} reglas CSS.`);
96
+ }
97
+ else {
98
+ console.log(`\n✅ Proceso completado. No se realizó ninguna eliminación.`);
99
+ }
100
+ if (deletedRules.length > 0 || keptRules.length > 0) {
101
+ try {
102
+ const reportPath = await (0, report_generator_1.generateFixReport)(projectPath, deletedRules, keptRules);
103
+ console.log(`\n📄 Informe de limpieza guardado en: ${path_1.default.relative(projectPath, reportPath)}`);
104
+ }
105
+ catch (error) {
106
+ console.error('\n❌ Ocurrió un error al guardar el informe de limpieza:', error);
107
+ }
108
+ }
109
+ }
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const analyze_1 = require("./commands/analyze");
9
+ const fix_1 = require("./commands/fix");
10
+ const package_json_1 = __importDefault(require("../package.json"));
11
+ const program = new commander_1.Command();
12
+ program
13
+ .name('vtex-css-sanitizer')
14
+ .description('Una CLI para limpiar clases CSS no usadas en proyectos VTEX IO')
15
+ .version(package_json_1.default.version);
16
+ program
17
+ .command('analyze')
18
+ .description('Analiza el proyecto y muestra las clases CSS y blockClass no utilizadas')
19
+ .argument('<path>', 'Ruta al directorio raíz del proyecto VTEX (ej: .)')
20
+ .action(analyze_1.analyzeCommand);
21
+ program
22
+ .command('fix')
23
+ .description('Elimina las reglas CSS no utilizadas de los archivos')
24
+ .argument('<path>', 'Ruta al directorio raíz del proyecto VTEX (ej: .)')
25
+ .action(fix_1.fixCommand);
26
+ program.parse(process.argv);
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractCssSuffixes = extractCssSuffixes;
7
+ exports.identifyRulesForDeletion = identifyRulesForDeletion;
8
+ const promises_1 = __importDefault(require("fs/promises"));
9
+ const postcss_1 = __importDefault(require("postcss"));
10
+ const VTEX_CLASS_REGEX = /\.([\w-]+(?:--[\w-]+)+)/g;
11
+ function getPrimarySuffix(vtexClassName) {
12
+ const parts = vtexClassName.split('--');
13
+ if (parts.length > 1) {
14
+ return parts[1];
15
+ }
16
+ return null;
17
+ }
18
+ async function extractCssSuffixes(cssFiles) {
19
+ const usedSuffixes = new Map();
20
+ for (const filePath of cssFiles) {
21
+ try {
22
+ const content = await promises_1.default.readFile(filePath, 'utf-8');
23
+ const root = postcss_1.default.parse(content);
24
+ root.walkRules(rule => {
25
+ rule.selectors.forEach(selector => {
26
+ const matches = [...selector.matchAll(VTEX_CLASS_REGEX)];
27
+ for (const match of matches) {
28
+ const fullClassName = match[0].substring(1);
29
+ const primarySuffix = getPrimarySuffix(fullClassName);
30
+ if (primarySuffix) {
31
+ const locations = usedSuffixes.get(primarySuffix) || [];
32
+ locations.push({ filePath, selector });
33
+ usedSuffixes.set(primarySuffix, locations);
34
+ }
35
+ }
36
+ });
37
+ });
38
+ }
39
+ catch (error) {
40
+ console.error(`❌ Error procesando el archivo CSS ${filePath}:`, error);
41
+ }
42
+ }
43
+ return usedSuffixes;
44
+ }
45
+ /**
46
+ * Identifica todas las reglas de CSS que son candidatas a ser eliminadas.
47
+ * No las elimina, solo las devuelve para su procesamiento interactivo.
48
+ */
49
+ function identifyRulesForDeletion(root, unusedSuffixes) {
50
+ const candidates = [];
51
+ root.walkRules(rule => {
52
+ const ruleSelectors = rule.selectors;
53
+ let allSelectorsAreUnused = ruleSelectors.length > 0;
54
+ for (const selector of ruleSelectors) {
55
+ const matches = [...selector.matchAll(VTEX_CLASS_REGEX)];
56
+ if (matches.length === 0) {
57
+ allSelectorsAreUnused = false;
58
+ break;
59
+ }
60
+ const hasAtLeastOneUsedSuffix = matches.some(match => {
61
+ const primarySuffix = getPrimarySuffix(match[0].substring(1));
62
+ return primarySuffix && !unusedSuffixes.has(primarySuffix);
63
+ });
64
+ if (hasAtLeastOneUsedSuffix) {
65
+ allSelectorsAreUnused = false;
66
+ break;
67
+ }
68
+ }
69
+ if (allSelectorsAreUnused) {
70
+ candidates.push(rule);
71
+ }
72
+ });
73
+ return candidates;
74
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.findJsonFiles = findJsonFiles;
7
+ exports.findCssFiles = findCssFiles;
8
+ const glob_1 = require("glob");
9
+ const path_1 = __importDefault(require("path"));
10
+ /**
11
+ * Encuentra todos los archivos .json y .jsonc en la carpeta /store.
12
+ */
13
+ function findJsonFiles(projectPath) {
14
+ const storePath = path_1.default.join(projectPath, 'store');
15
+ return (0, glob_1.sync)(`${storePath}/**/*.{json,jsonc}`);
16
+ }
17
+ /**
18
+ * Encuentra todos los archivos .css DE APPS NATIVAS (vtex.*) en la carpeta /styles/css.
19
+ * Omite los archivos de CSS de componentes custom para evitar falsos positivos.
20
+ */
21
+ function findCssFiles(projectPath) {
22
+ const stylesPath = path_1.default.join(projectPath, 'styles/css');
23
+ const allCssFiles = (0, glob_1.sync)(`${stylesPath}/**/*.css`);
24
+ const nativeVtexCssFiles = allCssFiles.filter(filePath => {
25
+ const fileName = path_1.default.basename(filePath);
26
+ return fileName.startsWith('vtex.');
27
+ });
28
+ return nativeVtexCssFiles;
29
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractBlockClasses = extractBlockClasses;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const jsonc_parser_1 = require("jsonc-parser");
9
+ /**
10
+ * Recorre todos los archivos JSON/JSONC para extraer las declaraciones de `blockClass`.
11
+ * @returns Un Map donde la clave es el valor de blockClass y el valor es dónde se encontró.
12
+ */
13
+ async function extractBlockClasses(jsonFiles) {
14
+ const declaredClasses = new Map();
15
+ for (const filePath of jsonFiles) {
16
+ try {
17
+ const content = await promises_1.default.readFile(filePath, 'utf-8');
18
+ const parsedJson = (0, jsonc_parser_1.parse)(content);
19
+ if (typeof parsedJson !== 'object' || parsedJson === null)
20
+ continue;
21
+ for (const blockName in parsedJson) {
22
+ if (!Object.prototype.hasOwnProperty.call(parsedJson, blockName))
23
+ continue;
24
+ const blockDef = parsedJson[blockName];
25
+ const blockClassValue = blockDef?.props?.blockClass;
26
+ let classesFound = [];
27
+ if (typeof blockClassValue === 'string') {
28
+ classesFound = blockClassValue.split(' ').filter(Boolean);
29
+ }
30
+ else if (Array.isArray(blockClassValue)) {
31
+ classesFound = blockClassValue.filter(cls => typeof cls === 'string' && cls.length > 0);
32
+ }
33
+ if (classesFound.length > 0) {
34
+ for (const cls of classesFound) {
35
+ const locations = declaredClasses.get(cls) || [];
36
+ locations.push({ filePath, blockName });
37
+ declaredClasses.set(cls, locations);
38
+ }
39
+ }
40
+ }
41
+ }
42
+ catch (error) {
43
+ console.error(`❌ Error procesando el archivo JSON ${filePath}:`, error);
44
+ }
45
+ }
46
+ return declaredClasses;
47
+ }
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateAnalysisReport = generateAnalysisReport;
7
+ exports.generateFixReport = generateFixReport;
8
+ const promises_1 = __importDefault(require("fs/promises"));
9
+ const path_1 = __importDefault(require("path"));
10
+ // --- Helper para crear el directorio de informes ---
11
+ async function ensureReportDirectory(projectPath) {
12
+ const reportDir = path_1.default.join(projectPath, '.sanitizer-reports');
13
+ await promises_1.default.mkdir(reportDir, { recursive: true }); // recursive: true evita errores si el dir ya existe
14
+ return reportDir;
15
+ }
16
+ // --- Helper para obtener la fecha en formato YYYY-MM-DD ---
17
+ function getFormattedDate() {
18
+ const date = new Date();
19
+ const year = date.getFullYear();
20
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
21
+ const day = date.getDate().toString().padStart(2, '0');
22
+ return `${year}-${month}-${day}`;
23
+ }
24
+ // --- Generador para el informe de 'analyze' ---
25
+ async function generateAnalysisReport(projectPath, unusedCss, unusedBlockClasses, usedCssSuffixesMap, declaredBlockClassesMap) {
26
+ const reportDir = await ensureReportDirectory(projectPath);
27
+ const reportPath = path_1.default.join(reportDir, `analysis-report-${getFormattedDate()}.md`);
28
+ let markdownContent = `# ✅ Informe de Análisis de CSS - ${getFormattedDate()}\n\n`;
29
+ markdownContent += `Este informe detalla las clases CSS y declaraciones \`blockClass\` que podrían estar sin uso en el proyecto.\n\n`;
30
+ markdownContent += `--- \n\n`;
31
+ // Sección 1: CSS sin uso
32
+ markdownContent += `## 🔴 Sufijos CSS sin \`blockClass\` correspondiente (${unusedCss.length} encontrados)\n\n`;
33
+ if (unusedCss.length > 0) {
34
+ unusedCss.forEach(suffix => {
35
+ markdownContent += `### \`--${suffix}\`\n\n`;
36
+ const locations = usedCssSuffixesMap.get(suffix) || [];
37
+ locations.slice(0, 5).forEach(loc => {
38
+ markdownContent += `* **Usado en:** \`${path_1.default.relative(projectPath, loc.filePath)}\`\n`;
39
+ markdownContent += `* **Selector:** \`${loc.selector}\`\n\n`;
40
+ });
41
+ });
42
+ }
43
+ else {
44
+ markdownContent += `¡Excelente! No se encontraron sufijos CSS sin uso.\n\n`;
45
+ }
46
+ markdownContent += `--- \n\n`;
47
+ // Sección 2: blockClass sin uso
48
+ markdownContent += `## 🟡 \`blockClass\` sin estilos CSS asociados (${unusedBlockClasses.length} encontrados)\n\n`;
49
+ if (unusedBlockClasses.length > 0) {
50
+ unusedBlockClasses.forEach(cls => {
51
+ markdownContent += `### \`"${cls}"\`\n\n`;
52
+ const locations = declaredBlockClassesMap.get(cls) || [];
53
+ locations.slice(0, 5).forEach(loc => {
54
+ markdownContent += `* **Declarado en:** \`${path_1.default.relative(projectPath, loc.filePath)}\`\n`;
55
+ markdownContent += `* **Bloque:** \`${loc.blockName}\`\n\n`;
56
+ });
57
+ });
58
+ }
59
+ else {
60
+ markdownContent += `¡Genial! Todas las \`blockClass\` declaradas se están utilizando.\n\n`;
61
+ }
62
+ await promises_1.default.writeFile(reportPath, markdownContent);
63
+ return reportPath;
64
+ }
65
+ async function generateFixReport(projectPath, deletedRules, keptRules) {
66
+ const reportDir = await ensureReportDirectory(projectPath);
67
+ const reportPath = path_1.default.join(reportDir, `fix-report-${getFormattedDate()}.md`);
68
+ let markdownContent = `# 🛠️ Informe de Limpieza de CSS - ${getFormattedDate()}\n\n`;
69
+ markdownContent += `Este informe detalla las acciones realizadas durante el proceso de limpieza interactiva.\n\n`;
70
+ markdownContent += `--- \n\n`;
71
+ // Sección 1: Reglas eliminadas
72
+ markdownContent += `## 🗑️ Reglas Eliminadas (${deletedRules.length})\n\n`;
73
+ if (deletedRules.length > 0) {
74
+ deletedRules.forEach(entry => {
75
+ markdownContent += `* **Archivo:** \`${path_1.default.relative(projectPath, entry.filePath)}\`\n`;
76
+ markdownContent += ` \`\`\`css\n ${entry.rule}\n \`\`\`\n\n`;
77
+ });
78
+ }
79
+ else {
80
+ markdownContent += `No se eliminó ninguna regla durante esta sesión.\n\n`;
81
+ }
82
+ markdownContent += `--- \n\n`;
83
+ // Sección 2: Reglas conservadas
84
+ markdownContent += `## 👍 Reglas Conservadas por el Usuario (${keptRules.length})\n\n`;
85
+ if (keptRules.length > 0) {
86
+ keptRules.forEach(entry => {
87
+ markdownContent += `* **Archivo:** \`${path_1.default.relative(projectPath, entry.filePath)}\`\n`;
88
+ markdownContent += ` \`\`\`css\n ${entry.rule}\n \`\`\`\n\n`;
89
+ });
90
+ }
91
+ else {
92
+ markdownContent += `No se conservó ninguna regla candidata durante esta sesión.\n\n`;
93
+ }
94
+ await promises_1.default.writeFile(reportPath, markdownContent);
95
+ return reportPath;
96
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "vtex-css-sanitizer-cli",
3
+ "version": "1.0.1",
4
+ "description": "Herramienta CLI para eliminar CSS y blockClass no utilizados en proyectos VTEX IO.",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "vtex-css-sanitizer": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "ts-node src/index.ts",
12
+ "dev": "npm run build && node dist/index.js"
13
+ },
14
+ "keywords": [
15
+ "vtex",
16
+ "vtexio",
17
+ "css",
18
+ "cleanup"
19
+ ],
20
+ "author": "Elias Daniel Emanuele",
21
+ "license": "ISC",
22
+ "dependencies": {
23
+ "@types/glob": "^8.1.0",
24
+ "commander": "^11.0.0",
25
+ "glob": "^8.1.0",
26
+ "jsonc-parser": "^3.2.0",
27
+ "postcss": "^8.4.29",
28
+ "prompts": "^2.4.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.5.7",
32
+ "@types/prompts": "^2.4.9",
33
+ "ts-node": "^10.9.1",
34
+ "typescript": "^5.2.2"
35
+ }
36
+ }