Skip to content

Commit

Permalink
✨ Find remnants
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthieuLemoine committed Oct 7, 2018
1 parent 29d4831 commit 73ba13e
Show file tree
Hide file tree
Showing 14 changed files with 395 additions and 13 deletions.
7 changes: 3 additions & 4 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@
},
"rules": {
"no-underscore-dangle": 0,
"no-use-before-define": [
"error",
{ "functions": false, "classes": true, "variables": true }
]
"no-use-before-define": ["error", { "functions": false, "classes": true, "variables": true }],
"import/no-dynamic-require": 0,
"max-len": 0
}
}
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ examples
.gitignore
.prettierignore
.prettierrc
assets
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Remnants [![CircleCI](https://circleci.com/gh/MatthieuLemoine/remnants/tree/master.svg?style=svg)](https://circleci.com/gh/MatthieuLemoine/remnants/tree/master)

Find unused files. Spot these residues, leftovers, relics of an ancient past.
Find unused files and dependencies. Spot these residues, leftovers, relics of an ancient past.

And :fire: them. Death to legacy & dead code :skull:

## Is it for me ?

:recycle: Did you recently refactor parts of your awesome project ? ✅

🧓 Is your project so old (more than 2 months old) that you can't even remember why some files exist ? ✅
🧓 Is your project so old (more than 2 months old) that you can't even remember why some files & dependencies exist ? ✅

🏭 Is your project so bloated that you're afraid to delete a file ? ✅

Expand All @@ -31,13 +31,35 @@ npm i -g remnants
In your project directory

```
remnants
remnants --sourceDirectories src
```

`sourceDirectories` are the folders where you want **Remnants** to look for unused files.

## Example

Running **Remnants** on itself 🤯

![screenshot](assets/failure.png)

:scream: Look at these remnants! :rage:

Let :fire: them all!

...

Done ✅

![screenshot](assets/screenshot.png)

Yeah no unused files or dependencies :tada:

Thanks **Remnants** !

## Advance usage

```
remnants
remnants --sourceDirectories src --sourceDirectories lib --projectRoot /Users/remnants/dev/awesome-project
```

## Related
Expand Down
17 changes: 17 additions & 0 deletions __tests__/readdir/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const path = require('path');
const { default: readdir } = require('../../src/readdir');

const sourceDirectory = path.join(__dirname, '..', '..', 'src');
const sourceDirectories = [sourceDirectory];

describe('readdir', () => {
test('should list all files recursively', async () => {
const files = await readdir(sourceDirectories);
expect(files[0].sort()).toEqual([
path.join(sourceDirectory, 'index.js'),
path.join(sourceDirectory, 'readdir', 'index.js'),
path.join(sourceDirectory, 'reporter', 'index.js'),
path.join(sourceDirectory, 'resolver', 'index.js'),
]);
});
});
Binary file added assets/failure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions index.js
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env node
/* eslint-disable */
require = require('esm')(module);
module.exports = require('./src/index');
5 changes: 4 additions & 1 deletion jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ const oldexecModule = jestRuntime.prototype._execModule;

jestRuntime.prototype._execModule = function _execModule(localModule, options) {
// Do not apply esm to dependencies & test files to have access to jest globals
if (localModule.id.includes('node_modules') || localModule.id.includes('__tests__')) {
if (
localModule.id.includes('node_modules') ||
localModule.id.includes('__tests__')
) {
return oldexecModule.apply(this, [localModule, options]);
}
localModule.exports = require('esm')(localModule)(localModule.id);
Expand Down
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@
"repository": "https://github.com/MatthieuLemoine/remnants",
"author": "MatthieuLemoine",
"license": "MIT",
"dependencies": {},
"bin": {
"remnants": "index.js"
},
"dependencies": {
"chalk": "^2.4.1",
"conductor": "^1.4.1",
"esm": "^3.0.84",
"ora": "^3.0.0",
"recursive-readdir": "^2.2.2",
"yargs": "^12.0.2"
},
"devDependencies": {
"eslint": "^5.6.1",
"eslint-config-airbnb-base": "^13.1.0",
Expand Down
36 changes: 36 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import path from 'path';
import yargs from 'yargs';
import ora from 'ora';
import { filesReport, dependenciesReport } from './reporter';
import resolve from './resolver';
import readdir from './readdir';

const { argv } = yargs.array('sourceDirectories');

const {
projectRoot: relativeRoot = process.cwd(),
sourceDirectories = [],
} = argv;

const projectRoot = path.resolve(relativeRoot);
const manifest = require(path.join(projectRoot, 'package.json'));

if (!sourceDirectories.length) {
process.stderr.write('Missing required argument --sourceDirectories\n');
process.exit(1);
}

const spinner = ora('Looking for remnants').start();

const { usedFiles, usedDependencies } = resolve(projectRoot);

(async () => {
const files = await readdir(sourceDirectories);
const unusedDependencies = [
...Object.keys(manifest.dependencies || {}),
].filter(item => !usedDependencies[item]);
const unusedFiles = files.map(item => item.filter(filePath => !usedFiles[path.join(projectRoot, filePath)]));
spinner.stop();
filesReport(projectRoot, sourceDirectories, unusedFiles);
dependenciesReport(unusedDependencies);
})();
3 changes: 3 additions & 0 deletions src/readdir/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import recursive from 'recursive-readdir';

export default (sources, exclude) => Promise.all(sources.map(directory => recursive(directory, exclude)));
41 changes: 41 additions & 0 deletions src/reporter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import path from 'path';
import chalk from 'chalk';

export const filesReport = (
projectRoot,
sourceDirectories,
filesByDirectory,
) => {
const allFiles = filesByDirectory.reduce(
(array, item) => array.concat(item),
[],
);
if (!allFiles.length) {
process.stdout.write(chalk.green('\n✅ No unused source files found\n'));
return;
}
process.stdout.write(
chalk.red(`\n❌ ${allFiles.length} unused source files found.\n`),
);
filesByDirectory.forEach((files, index) => {
const directory = sourceDirectories[index];
const relative = path.relative(projectRoot, directory);
process.stdout.write(chalk.blue(`\n● ${relative}\n`));
files.map(file => process.stdout.write(
chalk.yellow(` • ${path.relative(directory, file)}\n`),
));
});
};

export const dependenciesReport = (unusedDependencies) => {
if (!unusedDependencies.length) {
process.stdout.write(chalk.green('\n✅ No unused dependencies found.\n'));
return;
}
process.stdout.write(
chalk.red(
`\n❌ ${unusedDependencies.length} unused dependencies found.\n\n`,
),
);
unusedDependencies.forEach(dep => process.stdout.write(chalk.yellow(` • ${dep}\n`)));
};
94 changes: 94 additions & 0 deletions src/resolver/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import path from 'path';
import fs from 'fs';
import { compose } from 'conductor';

const extensions = ['.js', '.json', '.graphql', '.jsx'];
const imageExtensions = ['.png', '.jpg', '.jpeg'];
const indexFiles = [
'index.js',
'index.android.js',
'index.ios.js',
'index.web.js',
];
const importRegex = /from '(.*)'/g;
const requireRegex = /require\('(.*)'\)/g;
const commentRegex = /\/\/.*|\/\*.*\*\//g;
const graphqlImportRegex = /#import "(.*)"/g;
const usedFiles = {};
const usedDependencies = {};

const withExtensions = absolutePath => extensions.map(extension => `${absolutePath}${extension}`);
const withIndex = absolutePath => indexFiles.map(file => path.join(absolutePath, file));

const resolve = sourcePath => (relativePath) => {
const absolutePath = path.join(sourcePath, relativePath);
let paths = [];
// isFile
try {
fs.readdirSync(absolutePath);
} catch (e) {
paths.push(absolutePath);
const ext = path.extname(absolutePath);
if (imageExtensions.includes(ext)) {
paths.push(absolutePath.replace(ext, `@2x${ext}`));
paths.push(absolutePath.replace(ext, `@3x${ext}`));
}
}
paths = [
...paths,
...withExtensions(absolutePath),
...withIndex(absolutePath),
];
const founds = paths.filter(fs.existsSync);
return founds;
};

const findImports = filePaths => filePaths.map((filePath) => {
if (usedFiles[filePath]) {
return [];
}
usedFiles[filePath] = true;
const content = fs
.readFileSync(filePath, { encoding: 'utf8' })
.replace(commentRegex, '');
const founds = [];
let found = importRegex.exec(content);
while (found) {
if (found[1][0] === '.') {
founds.push(found[1]);
} else {
usedDependencies[found[1]] = true;
}
found = importRegex.exec(content);
}
found = requireRegex.exec(content);
while (found) {
if (found[1][0] === '.') {
founds.push(found[1]);
} else {
usedDependencies[found[1]] = true;
}
found = requireRegex.exec(content);
}
found = graphqlImportRegex.exec(content);
while (found) {
founds.push(found[1]);
found = graphqlImportRegex.exec(content);
}
return founds.map(resolveImports(path.dirname(filePath)));
});

function resolveImports(dirname) {
return compose(
findImports,
resolve(dirname),
);
}

export default (projectRoot) => {
resolveImports(projectRoot)('');
return {
usedFiles,
usedDependencies,
};
};
Loading

0 comments on commit 73ba13e

Please sign in to comment.