@noahnu/unused-files 0.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/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
+ }