@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 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>;
@@ -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,8 @@
1
+ export declare function walkDependencyTree(source: string, { aliases, visited, depth, }?: {
2
+ aliases?: Partial<Record<string, string>>;
3
+ visited?: Set<string>;
4
+ depth?: number;
5
+ }): AsyncGenerator<{
6
+ source: string;
7
+ dependency: string;
8
+ }, void, void>;
@@ -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));
@@ -0,0 +1,8 @@
1
+ import { Command } from 'clipanion';
2
+ export declare class BaseCommand extends Command {
3
+ entryFiles: string[];
4
+ ignorePatterns: string[] | undefined;
5
+ depth: number | undefined;
6
+ sourceDirectories: string[];
7
+ execute(): Promise<number | void>;
8
+ }
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
+ }