@noahnu/unused-files 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +11 -0
- package/README.md +33 -0
- package/lib/api/index.d.ts +30 -0
- package/lib/api/index.js +44 -0
- package/lib/api/walkDependencyTree.d.ts +8 -0
- package/lib/api/walkDependencyTree.js +99 -0
- package/lib/bin.d.ts +1 -0
- package/lib/bin.js +13 -0
- package/lib/command.d.ts +8 -0
- package/lib/command.js +63 -0
- package/package.json +46 -0
package/CHANGELOG.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
<!-- MONODEPLOY:BELOW -->
|
4
|
+
|
5
|
+
## [0.0.1](https://github.com/noahnu/nodejs-tools/compare/@noahnu/unused-files@0.0.0...@noahnu/unused-files@0.0.1) "@noahnu/unused-files" (2024-01-15)<a name="0.0.1"></a>
|
6
|
+
|
7
|
+
### Bug Fixes
|
8
|
+
|
9
|
+
* initial release ([f2f8e6b](https://github.com/noahnu/nodejs-tools/commits/f2f8e6b))
|
10
|
+
|
11
|
+
|
package/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# @noahnu/unused-files
|
2
|
+
|
3
|
+
## Usage
|
4
|
+
|
5
|
+
```sh
|
6
|
+
DEBUG=unused-files yarn dlx @noahnu/unused-files --entry src/index.ts --ignore '**/node_modules' --ignore '**/dist' --depth 10 ./src
|
7
|
+
```
|
8
|
+
|
9
|
+
Or use the Node API:
|
10
|
+
|
11
|
+
```ts
|
12
|
+
import { findUnusedFiles } from '@noahnu/unused-files'
|
13
|
+
|
14
|
+
const result = await findUnusedFiles({
|
15
|
+
entryFiles: ['src/index.ts'],
|
16
|
+
|
17
|
+
// optional
|
18
|
+
sourceDirectories: [process.cwd()],
|
19
|
+
ignorePatterns: ['**/node_modules'],
|
20
|
+
aliases: {
|
21
|
+
'@my/alias': 'path/to/file/index.ts',
|
22
|
+
},
|
23
|
+
depth: 10,
|
24
|
+
})
|
25
|
+
|
26
|
+
console.log(result.unusedFiles.join('\n'))
|
27
|
+
```
|
28
|
+
|
29
|
+
## Development
|
30
|
+
|
31
|
+
```sh
|
32
|
+
yarn workspace @noahnu/unused-files run-local
|
33
|
+
```
|
@@ -0,0 +1,30 @@
|
|
1
|
+
export type FindUnusedFilesOptions = {
|
2
|
+
/**
|
3
|
+
* Entry files into the codebase. These files are known to be used and any files that
|
4
|
+
* are dependencies of these entry files are also considered used files.
|
5
|
+
*/
|
6
|
+
entryFiles: string[];
|
7
|
+
/**
|
8
|
+
* Directories to search for files within. Relative to the current working directory.
|
9
|
+
*/
|
10
|
+
sourceDirectories?: string[];
|
11
|
+
/**
|
12
|
+
* Glob patterns used to exclude directories and files. Files which begin with
|
13
|
+
* a dot (i.e. hidden) are ignored by default.
|
14
|
+
*/
|
15
|
+
ignorePatterns?: string[];
|
16
|
+
/**
|
17
|
+
* Custom aliases that are consulted first before attempting to resolve the import path.
|
18
|
+
* It is recommended to rely on package.json aliases over these custom ones.
|
19
|
+
*/
|
20
|
+
aliases?: Partial<Record<string, string>>;
|
21
|
+
/**
|
22
|
+
* Maximum depth to traverse. -1 can be used to disable the depth limit (the default).
|
23
|
+
*/
|
24
|
+
depth?: number;
|
25
|
+
cwd?: string;
|
26
|
+
};
|
27
|
+
export type UnusedFilesResult = {
|
28
|
+
unusedFiles: string[];
|
29
|
+
};
|
30
|
+
export declare function findUnusedFiles({ entryFiles, ignorePatterns, sourceDirectories, aliases, depth, cwd, }: FindUnusedFilesOptions): Promise<UnusedFilesResult>;
|
package/lib/api/index.js
ADDED
@@ -0,0 +1,44 @@
|
|
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.findUnusedFiles = void 0;
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
8
|
+
const debug_1 = __importDefault(require("debug"));
|
9
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
10
|
+
const walkDependencyTree_1 = require("./walkDependencyTree");
|
11
|
+
const debug = (0, debug_1.default)('unused-files');
|
12
|
+
async function findUnusedFiles({ entryFiles, ignorePatterns = ['**/node_modules'], sourceDirectories = [], aliases, depth, cwd = process.cwd(), }) {
|
13
|
+
const globFromSource = (source) => fast_glob_1.default.glob(fast_glob_1.default.isDynamicPattern(source) ? source : node_path_1.default.join(source, '**'), {
|
14
|
+
dot: false,
|
15
|
+
ignore: ignorePatterns,
|
16
|
+
absolute: true,
|
17
|
+
cwd,
|
18
|
+
});
|
19
|
+
const sourceDirs = sourceDirectories.length > 0 ? sourceDirectories : [cwd];
|
20
|
+
const files = new Set([].concat(...(await Promise.all(sourceDirs.map((source) => globFromSource(source))))));
|
21
|
+
const unvisitedFiles = new Set(files);
|
22
|
+
for (const entryFile of entryFiles) {
|
23
|
+
const entry = node_path_1.default.resolve(cwd, entryFile);
|
24
|
+
unvisitedFiles.delete(entry);
|
25
|
+
for await (const { source, dependency } of (0, walkDependencyTree_1.walkDependencyTree)(entry, {
|
26
|
+
aliases,
|
27
|
+
depth,
|
28
|
+
})) {
|
29
|
+
if (files.has(dependency)) {
|
30
|
+
debug(`${source}: ${dependency} [dependency]`);
|
31
|
+
}
|
32
|
+
else {
|
33
|
+
debug(`${source}: ${dependency} [unknown dependency]`);
|
34
|
+
}
|
35
|
+
unvisitedFiles.delete(dependency);
|
36
|
+
}
|
37
|
+
}
|
38
|
+
return {
|
39
|
+
unusedFiles: Array.from(unvisitedFiles)
|
40
|
+
.map((abspath) => node_path_1.default.relative(cwd, abspath))
|
41
|
+
.sort(),
|
42
|
+
};
|
43
|
+
}
|
44
|
+
exports.findUnusedFiles = findUnusedFiles;
|
@@ -0,0 +1,99 @@
|
|
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.walkDependencyTree = void 0;
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
9
|
+
const typescript_estree_1 = require("@typescript-eslint/typescript-estree");
|
10
|
+
const debug_1 = __importDefault(require("debug"));
|
11
|
+
const debug = (0, debug_1.default)('unused-files:parse');
|
12
|
+
const DEFAULT_DEPTH_LIMIT = -1; // no depth limit
|
13
|
+
const VALID_EXTENSIONS = new Set(['ts', 'tsx', 'mts', 'cts', 'js', 'mjs', 'cjs']);
|
14
|
+
async function* walkDependencyTree(source, { aliases, visited, depth = DEFAULT_DEPTH_LIMIT, } = {}) {
|
15
|
+
const ext = node_path_1.default.extname(source).substring(1);
|
16
|
+
if (!VALID_EXTENSIONS.has(ext)) {
|
17
|
+
debug(`${source}: Unknown file extension '${ext}' [skipping]`);
|
18
|
+
return;
|
19
|
+
}
|
20
|
+
const visitedSet = visited ?? new Set();
|
21
|
+
if (visitedSet.has(source))
|
22
|
+
return;
|
23
|
+
visitedSet.add(source);
|
24
|
+
const code = await node_fs_1.default.promises.readFile(source, { encoding: 'utf-8' });
|
25
|
+
const ast = (0, typescript_estree_1.parse)(code, {
|
26
|
+
allowInvalidAST: true,
|
27
|
+
comment: false,
|
28
|
+
suppressDeprecatedPropertyWarnings: true,
|
29
|
+
errorOnUnknownASTType: false,
|
30
|
+
filePath: source,
|
31
|
+
jsDocParsingMode: 'none',
|
32
|
+
});
|
33
|
+
const importFroms = new Set();
|
34
|
+
const visitors = {
|
35
|
+
[typescript_estree_1.TSESTree.AST_NODE_TYPES.ImportDeclaration]: (node) => {
|
36
|
+
if (node.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.ImportDeclaration) {
|
37
|
+
importFroms.add(node.source.value);
|
38
|
+
}
|
39
|
+
},
|
40
|
+
[typescript_estree_1.TSESTree.AST_NODE_TYPES.CallExpression]: (node) => {
|
41
|
+
if (node.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.CallExpression &&
|
42
|
+
node.callee.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.Identifier &&
|
43
|
+
node.callee.name === 'require') {
|
44
|
+
const arg = node.arguments[0];
|
45
|
+
if (arg.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.Literal) {
|
46
|
+
if (typeof arg.value === 'string') {
|
47
|
+
importFroms.add(arg.value);
|
48
|
+
}
|
49
|
+
}
|
50
|
+
else {
|
51
|
+
debug(`${source}: Dynamic require expression found at ${node.loc.start}:${node.loc.end}`);
|
52
|
+
}
|
53
|
+
}
|
54
|
+
},
|
55
|
+
[typescript_estree_1.TSESTree.AST_NODE_TYPES.ImportExpression]: (node) => {
|
56
|
+
if (node.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.ImportExpression) {
|
57
|
+
if (node.source.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.Literal) {
|
58
|
+
if (typeof node.source.value === 'string') {
|
59
|
+
importFroms.add(node.source.value);
|
60
|
+
}
|
61
|
+
}
|
62
|
+
else {
|
63
|
+
debug(`${source}: Dynamic import expression found at ${node.loc.start}:${node.loc.end}`);
|
64
|
+
}
|
65
|
+
}
|
66
|
+
},
|
67
|
+
};
|
68
|
+
for (const body of ast.body) {
|
69
|
+
(0, typescript_estree_1.simpleTraverse)(body, { visitors });
|
70
|
+
}
|
71
|
+
const resolveToAbsPath = (request) => {
|
72
|
+
const aliasedPath = aliases?.[request];
|
73
|
+
if (aliasedPath) {
|
74
|
+
return node_path_1.default.resolve(aliasedPath);
|
75
|
+
}
|
76
|
+
try {
|
77
|
+
return require.resolve(request, { paths: [node_path_1.default.dirname(source)] });
|
78
|
+
}
|
79
|
+
catch { }
|
80
|
+
return undefined;
|
81
|
+
};
|
82
|
+
for (const importFrom of Array.from(importFroms)) {
|
83
|
+
const absPath = resolveToAbsPath(importFrom);
|
84
|
+
if (absPath) {
|
85
|
+
yield { dependency: absPath, source };
|
86
|
+
if (depth === -1 || depth > 0) {
|
87
|
+
yield* walkDependencyTree(absPath, {
|
88
|
+
aliases,
|
89
|
+
visited: visitedSet,
|
90
|
+
depth: depth === -1 ? depth : depth - 1,
|
91
|
+
});
|
92
|
+
}
|
93
|
+
}
|
94
|
+
else {
|
95
|
+
debug(`${source}: Unable to resolve '${importFrom}'`);
|
96
|
+
}
|
97
|
+
}
|
98
|
+
}
|
99
|
+
exports.walkDependencyTree = walkDependencyTree;
|
package/lib/bin.d.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
package/lib/bin.js
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
const clipanion_1 = require("clipanion");
|
4
|
+
const command_1 = require("./command");
|
5
|
+
const cli = new clipanion_1.Cli({
|
6
|
+
binaryLabel: '@noahnu/unused-files',
|
7
|
+
binaryName: 'yarn @noahnu/unused-files',
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
9
|
+
binaryVersion: require('../package.json').version,
|
10
|
+
enableCapture: true,
|
11
|
+
});
|
12
|
+
cli.register(command_1.BaseCommand);
|
13
|
+
cli.runExit(process.argv.slice(2));
|
package/lib/command.d.ts
ADDED
package/lib/command.js
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
19
|
+
if (mod && mod.__esModule) return mod;
|
20
|
+
var result = {};
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
22
|
+
__setModuleDefault(result, mod);
|
23
|
+
return result;
|
24
|
+
};
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
26
|
+
exports.BaseCommand = void 0;
|
27
|
+
const clipanion_1 = require("clipanion");
|
28
|
+
const t = __importStar(require("typanion"));
|
29
|
+
const api_1 = require("./api");
|
30
|
+
class BaseCommand extends clipanion_1.Command {
|
31
|
+
entryFiles = clipanion_1.Option.Array('--entry', {
|
32
|
+
description: 'Entry files into the codebase. These files are known to be ' +
|
33
|
+
'used and any files that are dependencies of these entry files ' +
|
34
|
+
'are also considered used files.',
|
35
|
+
required: true,
|
36
|
+
});
|
37
|
+
ignorePatterns = clipanion_1.Option.Array('--ignore', {
|
38
|
+
description: 'Glob patterns usued to exclude files. ' +
|
39
|
+
'The patterns are applied during traversal ' +
|
40
|
+
'of the directory tree.',
|
41
|
+
required: false,
|
42
|
+
});
|
43
|
+
depth = clipanion_1.Option.String('--depth', {
|
44
|
+
description: 'Depth limit. Set to -1 to disable.',
|
45
|
+
required: false,
|
46
|
+
validator: t.isNumber(),
|
47
|
+
});
|
48
|
+
sourceDirectories = clipanion_1.Option.Rest();
|
49
|
+
async execute() {
|
50
|
+
const result = await (0, api_1.findUnusedFiles)({
|
51
|
+
entryFiles: this.entryFiles,
|
52
|
+
ignorePatterns: this.ignorePatterns,
|
53
|
+
sourceDirectories: this.sourceDirectories.length
|
54
|
+
? this.sourceDirectories
|
55
|
+
: [process.cwd()],
|
56
|
+
depth: typeof this.depth === 'undefined'
|
57
|
+
? -1
|
58
|
+
: parseInt(Math.max(this.depth, -1).toFixed(0), 10),
|
59
|
+
});
|
60
|
+
this.context.stdout.write(result.unusedFiles.join('\n'));
|
61
|
+
}
|
62
|
+
}
|
63
|
+
exports.BaseCommand = BaseCommand;
|
package/package.json
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
{
|
2
|
+
"name": "@noahnu/unused-files",
|
3
|
+
"version": "0.0.1",
|
4
|
+
"repository": {
|
5
|
+
"type": "git",
|
6
|
+
"url": "https://github.com/noahnu/nodejs-tools.git",
|
7
|
+
"directory": "packages/unused-files"
|
8
|
+
},
|
9
|
+
"license": "MIT",
|
10
|
+
"author": {
|
11
|
+
"name": "noahnu",
|
12
|
+
"email": "noah@noahnu.com",
|
13
|
+
"url": "https://noahnu.com"
|
14
|
+
},
|
15
|
+
"scripts": {
|
16
|
+
"clean": "run workspace:clean \"$(pwd)\"",
|
17
|
+
"prepack": "run workspace:build \"$(pwd)\"",
|
18
|
+
"run-local": "run -T ts-node --transpileOnly ./src/bin.ts"
|
19
|
+
},
|
20
|
+
"bin": "./lib/bin.js",
|
21
|
+
"main": "./lib/api/index.js",
|
22
|
+
"publishConfig": {
|
23
|
+
"registry": "https://registry.npmjs.org/",
|
24
|
+
"access": "public",
|
25
|
+
"bin": "./lib/bin.js",
|
26
|
+
"main": "./lib/api/index.js",
|
27
|
+
"types": "./lib/api/index.d.ts"
|
28
|
+
},
|
29
|
+
"files": [
|
30
|
+
"lib"
|
31
|
+
],
|
32
|
+
"dependencies": {
|
33
|
+
"@types/debug": "^4.1.12",
|
34
|
+
"@typescript-eslint/typescript-estree": "^6.18.1",
|
35
|
+
"clipanion": "4.0.0-rc.2",
|
36
|
+
"debug": "^4.3.4",
|
37
|
+
"fast-glob": "^3.3.2",
|
38
|
+
"typanion": "^3.14.0"
|
39
|
+
},
|
40
|
+
"devDependencies": {
|
41
|
+
"@jest/globals": "^29.7.0",
|
42
|
+
"@noahnu/internal-test-utils": "0.0.0",
|
43
|
+
"@types/node": "^20.9.0"
|
44
|
+
},
|
45
|
+
"types": "./lib/api/index.d.ts"
|
46
|
+
}
|