qleaner 1.0.23 โ†’ 1.0.25

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 CHANGED
@@ -5,6 +5,7 @@ A powerful CLI tool to analyze and clean up your React codebase by finding unuse
5
5
  ## Features
6
6
 
7
7
  - ๐Ÿ” **Scan for unused files**: Identify files that are not imported anywhere in your project
8
+ - ๐Ÿ–ผ๏ธ **Scan for unused images**: Find image files that are not referenced in your codebase
8
9
  - ๐Ÿ“‹ **List all imports**: Get a complete list of all import statements in your codebase with file locations
9
10
  - ๐ŸŽฏ **File listing**: List all files in your project
10
11
  - ๐Ÿ“Š **Table output**: Display results in formatted tables for better readability
@@ -13,6 +14,7 @@ A powerful CLI tool to analyze and clean up your React codebase by finding unuse
13
14
  - ๐Ÿ’ช **TypeScript support**: Works with TypeScript, JavaScript, JSX, and TSX files
14
15
  - ๐Ÿงช **Dry run mode**: Preview what would be deleted without actually deleting files
15
16
  - ๐Ÿ’พ **Smart caching**: Caches scan results for faster subsequent runs (automatically invalidates on file changes)
17
+ - ๐ŸŽจ **CSS and styled-components support**: Detects images used in CSS files and styled-components
16
18
 
17
19
  ## Installation
18
20
 
@@ -117,6 +119,63 @@ qleaner qlean-scan src --clear-cache
117
119
  qleaner qlean-scan src --clear-cache --dry-run
118
120
  ```
119
121
 
122
+ ### Scan for Unused Images
123
+
124
+ Find image files that are not referenced anywhere in your codebase:
125
+
126
+ ```bash
127
+ qleaner qlean-image <directory> <rootPath> [options]
128
+ ```
129
+
130
+ **Arguments:**
131
+ - `<directory>` - The path to the directory containing image files to scan
132
+ - `<rootPath>` - The root path to the project code that utilizes the images
133
+
134
+ **Options:**
135
+ - `-e, --exclude-dir-assets <dir...>` - Exclude directories from the asset scan
136
+ - `-f, --exclude-file-asset <file...>` - Exclude files from the asset scan
137
+ - `-F, --exclude-file-print-asset <files...>` - Scan but don't print the excluded asset files
138
+ - `-E, --exclude-dir-code <dir...>` - Exclude directories from the code scan
139
+ - `-S, --exclude-file-code <file...>` - Exclude files from the code scan
140
+ - `-P, --exclude-file-print-code <files...>` - Scan but don't print the excluded code files
141
+ - `-t, --table` - Display results in a formatted table
142
+ - `-d, --dry-run` - Show what would be deleted without actually deleting (skips prompt)
143
+ - `-C, --clear-cache` - Clear the cache before scanning
144
+
145
+ **Examples:**
146
+
147
+ ```bash
148
+ # Scan for unused images in public/images directory
149
+ qleaner qlean-image public/images src
150
+
151
+ # Display unused images in a table format
152
+ qleaner qlean-image public/images src --table
153
+
154
+ # Scan with exclusions for assets
155
+ qleaner qlean-image public/images src -e public/images/icons
156
+
157
+ # Scan with exclusions for code files
158
+ qleaner qlean-image public/images src -E __tests__ node_modules
159
+
160
+ # Dry run - preview what would be deleted
161
+ qleaner qlean-image public/images src --dry-run
162
+
163
+ # Dry run with table output
164
+ qleaner qlean-image public/images src --dry-run --table
165
+
166
+ # Scan with multiple exclusions
167
+ qleaner qlean-image public/images src -e public/images/icons -E __tests__ -S "**/*.test.*"
168
+ ```
169
+
170
+ **What it detects:**
171
+ - Images imported via `import` statements
172
+ - Images required via `require()` calls
173
+ - Images used in JSX `src` attributes
174
+ - Images in CSS `url()` functions (CSS and SCSS files)
175
+ - Images in styled-components and CSS-in-JS template literals
176
+ - Images in inline styles (backgroundImage, etc.)
177
+ - Images referenced in string literals and template literals
178
+
120
179
  ## Output Formats
121
180
 
122
181
  Qleaner provides two output formats:
@@ -131,9 +190,12 @@ Qleaner provides two output formats:
131
190
  - Import tables show: File, Line, Column, and Import path
132
191
  - File tables show: File path
133
192
  - Unused files table shows: Unused file paths (or "Would Delete" in dry run mode)
193
+ - Unused images table shows: Unused image paths (or "Would Delete" in dry run mode)
134
194
 
135
195
  ## How It Works
136
196
 
197
+ ### File Scanning (qlean-scan)
198
+
137
199
  1. **File Discovery**: Recursively finds all `.tsx`, `.ts`, `.js`, and `.jsx` files in the specified directory
138
200
  2. **Caching**: Checks cache for previously scanned files. Files that haven't changed are skipped for faster performance
139
201
  3. **Import Extraction**: Parses files using Babel AST and extracts all import statements (only for changed files or cache misses)
@@ -143,19 +205,49 @@ Qleaner provides two output formats:
143
205
  7. **Reporting**: Outputs the results in standard or table format based on your preferences
144
206
  8. **Safe Deletion**: In dry run mode, shows what would be deleted without making changes. In normal mode, prompts for confirmation before deletion
145
207
 
208
+ ### Image Scanning (qlean-image)
209
+
210
+ 1. **Image Discovery**: Recursively finds all image files (`.png`, `.jpg`, `.jpeg`, `.svg`, `.gif`, `.webp`) in the specified directory
211
+ 2. **Code Scanning**: Scans all code files (`.js`, `.jsx`, `.ts`, `.tsx`) for image references
212
+ 3. **Image Detection**: Detects images through multiple methods:
213
+ - Import statements (`import img from './image.png'`)
214
+ - Require calls (`require('./image.png')`)
215
+ - JSX src attributes (`<img src="./image.png" />`)
216
+ - CSS url() functions in CSS/SCSS files
217
+ - Styled-components and CSS-in-JS template literals
218
+ - Inline styles (backgroundImage, etc.)
219
+ - String and template literals containing image paths
220
+ 4. **Path Normalization**: Normalizes all detected image paths for consistent matching
221
+ 5. **Analysis**: Compares discovered image files with detected references to identify unused images
222
+ 6. **Reporting**: Outputs the results in standard or table format
223
+ 7. **Safe Deletion**: In dry run mode, shows what would be deleted without making changes. In normal mode, prompts for confirmation before deletion
224
+
146
225
  ## Supported File Types
147
226
 
227
+ **Code files:**
148
228
  - `.js` - JavaScript files
149
229
  - `.jsx` - JavaScript React files
150
230
  - `.ts` - TypeScript files
151
231
  - `.tsx` - TypeScript React files
152
232
 
233
+ **Image files (for qlean-image):**
234
+ - `.png` - PNG images
235
+ - `.jpg`, `.jpeg` - JPEG images
236
+ - `.svg` - SVG images
237
+ - `.gif` - GIF images
238
+ - `.webp` - WebP images
239
+
240
+ **CSS files (for image detection):**
241
+ - `.css` - CSS files
242
+ - `.scss` - SCSS files
243
+
153
244
  ## Use Cases
154
245
 
155
246
  - ๐Ÿงน **Code cleanup**: Remove dead code and unused files from your React projects
247
+ - ๐Ÿ–ผ๏ธ **Image cleanup**: Find and remove unused image assets to reduce project size
156
248
  - ๐Ÿ“Š **Code analysis**: Understand import patterns and dependencies in your codebase
157
- - ๐Ÿ” **Project audit**: Identify orphaned files that may have been forgotten
158
- - ๐Ÿ“ฆ **Bundle optimization**: Find files that can be removed to reduce bundle size
249
+ - ๐Ÿ” **Project audit**: Identify orphaned files and assets that may have been forgotten
250
+ - ๐Ÿ“ฆ **Bundle optimization**: Find files and images that can be removed to reduce bundle size
159
251
  - ๐ŸŽฏ **Maintenance**: Keep your codebase clean and maintainable
160
252
 
161
253
  ## Configuration
@@ -216,4 +308,4 @@ MIT
216
308
 
217
309
  ## Version
218
310
 
219
- Current version: 1.0.13
311
+ Current version: 1.0.25
package/command.js CHANGED
@@ -4,7 +4,6 @@ const Table = require("cli-table3");
4
4
  const parser = require("@babel/parser");
5
5
  const traverse = require("@babel/traverse").default;
6
6
  const path = require("path");
7
- const cliProgress = require("cli-progress");
8
7
  const { createResolver } = require("./utils/resolver");
9
8
  const {
10
9
  loadCache,
@@ -15,14 +14,6 @@ const {
15
14
  const { isExcludedFile, compareFiles } = require("./utils/utils");
16
15
 
17
16
  async function getFiles(directory = "src", options, chalk) {
18
- const bars = new cliProgress.MultiBar({
19
- clearOnComplete: false,
20
- hideCursor: true,
21
- format: chalk.cyan('{name}') +
22
- ' |' + '{bar}' + '| ' +
23
- '{value}/{total} files || {status}'
24
- }, cliProgress.Presets.shades_grey);
25
-
26
17
  const contentPaths = [`${directory}/**/*.{tsx,ts,js,jsx}`];
27
18
  if (options.excludeDir && options.excludeDir.length > 0) {
28
19
  options.excludeDir.forEach((dir) => {
@@ -38,12 +29,11 @@ async function getFiles(directory = "src", options, chalk) {
38
29
  const files = await fg(contentPaths);
39
30
  const imports = [];
40
31
 
41
- const scanBar = bars.create(files.length, 0, {
42
- name: 'Scanning Files',
43
- status: 'Scanning...',
44
- });
45
-
32
+ let index = 0;
46
33
  for (const file of files) {
34
+ index++;
35
+ console.clear();
36
+ console.log('Scanning file...', index, 'of', files.length);
47
37
  const code = fs.readFileSync(file, "utf8");
48
38
  const ast = parser.parse(code, {
49
39
  sourceType: "module",
@@ -71,10 +61,7 @@ async function getFiles(directory = "src", options, chalk) {
71
61
  // }
72
62
  // },
73
63
  // });
74
- scanBar.increment();
75
64
  }
76
-
77
- bars.stop();
78
65
 
79
66
  if (options.table) {
80
67
  const tableImports = new Table({
@@ -125,14 +112,6 @@ async function getFiles(directory = "src", options, chalk) {
125
112
  }
126
113
 
127
114
  async function unUsedFiles(chalk, directory = "src", options) {
128
- const bars = new cliProgress.MultiBar({
129
- clearOnComplete: false,
130
- hideCursor: true,
131
- format: chalk.cyan('{name}') +
132
- ' |' + '{bar}' + '| ' +
133
- '{value}/{total} files || {status}'
134
- }, cliProgress.Presets.shades_grey);
135
-
136
115
  const resolver = createResolver(directory);
137
116
  const cache = loadCache(process.cwd());
138
117
  const contentPaths = [`${directory}/**/*.{tsx,ts,js,jsx}`];
@@ -151,15 +130,13 @@ async function unUsedFiles(chalk, directory = "src", options) {
151
130
  let imports = [];
152
131
  const unusedFiles = [];
153
132
 
154
- const scanBar = bars.create(files.length, 0, {
155
- name: 'Scanning Files',
156
- status: 'Scanning...',
157
- });
158
-
159
133
  // debug log
160
134
  // let debugCount = 0;
161
- let i = 0;
135
+ let index = 0;
162
136
  for (const file of files) {
137
+ index++;
138
+ console.clear();
139
+ console.log('Checking file...', index, 'of', files.length);
163
140
  const code = fs.readFileSync(file, "utf8");
164
141
  if (needsRebuild(file, code, cache)) {
165
142
  const ast = parser.parse(code, {
@@ -193,9 +170,6 @@ async function unUsedFiles(chalk, directory = "src", options) {
193
170
  // debugCount++;
194
171
  // console.log('cache hit', debugCount);
195
172
  }
196
- scanBar.update(++i, {
197
- status: 'Scanning...',
198
- });
199
173
  }
200
174
 
201
175
  // imports beings empty shows all the files were cached
@@ -203,14 +177,13 @@ async function unUsedFiles(chalk, directory = "src", options) {
203
177
  imports = cache[directory]? cache[directory].imports: [];
204
178
  }
205
179
 
206
- const checkBar = bars.create(files.length, 0, {
207
- name: 'Checking Usage',
208
- status: 'Checking...',
209
- });
210
180
 
211
181
  // debugCount = 0;
212
- i = 0;
182
+ index = 0;
213
183
  for (const file of files) {
184
+ index++;
185
+ console.clear();
186
+ console.log('Checking file...', index, 'of', files.length);
214
187
  const code = fs.readFileSync(file, "utf8");
215
188
  if (!cache[file].isImported || needsRebuild(file, code, cache)) {
216
189
  let i = 0;
@@ -240,12 +213,8 @@ async function unUsedFiles(chalk, directory = "src", options) {
240
213
  // debugCount++;
241
214
  // console.log("debug hit", debugCount);
242
215
  }
243
- checkBar.update(++i, {
244
- status: 'Checking...',
245
- });
246
216
  }
247
217
 
248
- bars.stop();
249
218
  cache[directory] = {
250
219
  imports: imports,
251
220
  }
@@ -6,7 +6,6 @@ const traverse = require("@babel/traverse").default;
6
6
  const Table = require("cli-table3");
7
7
  const { getCssImages } = require("../utils/cssImages");
8
8
  const { askDeleteFiles } = require("../utils/utils");
9
- const cliProgress = require("cli-progress");
10
9
 
11
10
  // Regex for matching url("x.png") or url('x.png') or bg-[url('x.png')]
12
11
  const URL_EXTRACT_REGEX = /url\((['"]?)([^"')]+)\1\)/gi;
@@ -108,14 +107,6 @@ function extractFromJSXStyle(node, file, collected) {
108
107
  * MAIN FUNCTION
109
108
  */
110
109
  async function getUnusedImages(chalk, imageDirectory, codeDirectory, options) {
111
- const bars = new cliProgress.MultiBar({
112
- clearOnComplete: false,
113
- hideCursor: true,
114
- format: chalk.cyan('{name}') +
115
- ' |' + '{bar}' + '| ' +
116
- '{value}/{total} files || {status}'
117
- }, cliProgress.Presets.shades_grey)
118
-
119
110
  const used = new Set();
120
111
  const unusedImages = [];
121
112
 
@@ -124,13 +115,11 @@ async function getUnusedImages(chalk, imageDirectory, codeDirectory, options) {
124
115
 
125
116
  // ---- Scan Code Files ----
126
117
  const codeFiles = await fg([`${codeDirectory}/**/*.{js,jsx,ts,tsx}`]);
127
- const codeBar = bars.create(codeFiles.length, 0, {
128
- name: 'Code Files',
129
- status: 'Scanning...',
130
- });
131
-
132
-
118
+ let index = 0;
133
119
  for (const file of codeFiles) {
120
+ index++;
121
+ console.clear();
122
+ console.log('Scanning code files...', index, 'of', codeFiles.length);
134
123
  const code = fs.readFileSync(file, "utf8");
135
124
 
136
125
  const ast = parser.parse(code, {
@@ -253,41 +242,37 @@ async function getUnusedImages(chalk, imageDirectory, codeDirectory, options) {
253
242
  extracted.forEach((v) => used.add(JSON.stringify({ path: v, file })));
254
243
  },
255
244
  });
256
- codeBar.increment();
257
245
  }
258
246
 
259
247
  // Add CSS images
260
- const cssImages = await getCssImages(codeDirectory, bars);
248
+ const cssImages = await getCssImages(codeDirectory);
261
249
  cssImages.forEach((img) =>
262
250
  used.add(JSON.stringify({ path: img.value, file: img.file }))
263
251
  );
264
- const normalizeBar = bars.create(used.size, 0, {
265
- name: 'Normalizing Images',
266
- status: 'Normalizing...',
267
- });
268
252
 
269
253
  // Normalize all used images
270
254
  const normalizedUsed = new Set();
255
+ index = 0;
271
256
  for (const entry of used) {
272
- normalizeBar.increment();
257
+ index++;
258
+ console.clear();
259
+ console.log('Normalizing images...', index, 'of', used.size);
273
260
  const { path: p } = JSON.parse(entry);
274
261
  const normalized = normalize(p, imageDirectory);
275
262
  if (normalized) normalizedUsed.add(normalized);
276
263
  }
277
264
 
278
- const unusedBar = bars.create(imageFiles.length, 0, {
279
- name: 'Determining Unused Images',
280
- status: 'Determining...',
281
- });
282
265
  // ---- Determine unused ----
266
+ index = 0;
283
267
  for (const img of imageFiles) {
284
- unusedBar.increment();
268
+ index++;
269
+ console.clear();
270
+ console.log('Determining unused images...', index, 'of', imageFiles.length);
285
271
  const full = path.resolve(img);
286
272
  if (!normalizedUsed.has(full)) {
287
273
  unusedImages.push(full);
288
274
  }
289
275
  }
290
- bars.stop();
291
276
  // ---- Output table or list ----
292
277
  if (options.table) {
293
278
  const table = new Table({
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "qleaner",
3
3
  "packageManager": "yarn@4.6.0",
4
- "version": "1.0.23",
4
+ "version": "1.0.25",
5
5
  "main": "command.js",
6
6
  "bin": "./bin/cli.js",
7
7
  "scripts": {
8
8
  "start": "node ./bin/cli.js",
9
- "resolve": "node ./utils/resolver.js"
9
+ "resolve": "node ./utils/resolver.js",
10
+ "scan": "node ./scanning.js"
10
11
  },
11
12
  "devDependencies": {
12
13
  "@types/node": "^20.10.5",
@@ -20,17 +20,16 @@ function extractCssImages(cssContent, images, file) {
20
20
  return images
21
21
  }
22
22
 
23
- async function getCssImages(directory = "src", bars) {
23
+ async function getCssImages(directory = "src") {
24
24
  const cssFiles = await fg([
25
25
  `${directory}/**/*.{css,scss}`,
26
26
  ]);
27
27
  let images = [];
28
- const cssBar = bars.create(cssFiles.length, 0, {
29
- name: 'CSS Files',
30
- status: 'Scanning...',
31
- });
28
+ let index = 0;
32
29
  for (const file of cssFiles) {
33
- cssBar.increment();
30
+ index++;
31
+ console.clear();
32
+ console.log('Scanning CSS files...', index, 'of', cssFiles.length);
34
33
  const css = fs.readFileSync(file, "utf-8");
35
34
  images = extractCssImages(css, images, file);
36
35
  }