@noahnu/unused-files 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  # Changelog
2
2
 
3
- <!-- MONODEPLOY:BELOW -->
3
+ <!-- MONOWEAVE:BELOW -->
4
+
5
+ ## @noahnu/unused-files (v0.1.0) <a name="0.1.0"></a>
6
+
7
+ Support custom resolvers.
8
+
9
+
10
+
11
+ ## @noahnu/unused-files (v0.0.2) <a name="0.0.2"></a>
12
+
13
+ Update ESLint config and plugins to ESLint v9.
14
+
15
+ Update misc. dependencies.
16
+
17
+
4
18
 
5
19
  ## [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
20
 
package/README.md CHANGED
@@ -17,9 +17,14 @@ const result = await findUnusedFiles({
17
17
  // optional
18
18
  sourceDirectories: [process.cwd()],
19
19
  ignorePatterns: ['**/node_modules'],
20
- aliases: {
21
- '@my/alias': 'path/to/file/index.ts',
22
- },
20
+ resolvers: [
21
+ async ({ request, context }) => {
22
+ if (request === '@my/alias') {
23
+ return { result: 'path/to/file/index.ts' }
24
+ }
25
+ return null;
26
+ }
27
+ ],
23
28
  depth: 10,
24
29
  })
25
30
 
@@ -1,4 +1,6 @@
1
- export type FindUnusedFilesOptions = {
1
+ import { type Resolver } from './types';
2
+ export type { Resolver, ResolverResult, ResolverParams } from './types';
3
+ export interface FindUnusedFilesOptions {
2
4
  /**
3
5
  * Entry files into the codebase. These files are known to be used and any files that
4
6
  * are dependencies of these entry files are also considered used files.
@@ -14,17 +16,19 @@ export type FindUnusedFilesOptions = {
14
16
  */
15
17
  ignorePatterns?: string[];
16
18
  /**
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
+ * Custom resolver to use to resolve an import path relative to a source file.
20
+ * It is recommended to rely on package.json aliases over these custom ones
21
+ * when possible.
19
22
  */
20
- aliases?: Partial<Record<string, string>>;
23
+ resolvers?: Resolver[];
21
24
  /**
22
25
  * Maximum depth to traverse. -1 can be used to disable the depth limit (the default).
23
26
  */
24
27
  depth?: number;
25
28
  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>;
29
+ }
30
+ export interface UnusedFilesResult {
31
+ used: string[];
32
+ unused: string[];
33
+ }
34
+ export declare function findUnusedFiles({ entryFiles, ignorePatterns, sourceDirectories, resolvers, depth, cwd, }: FindUnusedFilesOptions): Promise<UnusedFilesResult>;
package/lib/api/index.js CHANGED
@@ -3,42 +3,58 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.findUnusedFiles = void 0;
6
+ exports.findUnusedFiles = findUnusedFiles;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
7
8
  const node_path_1 = __importDefault(require("node:path"));
8
9
  const debug_1 = __importDefault(require("debug"));
9
10
  const fast_glob_1 = __importDefault(require("fast-glob"));
10
11
  const walkDependencyTree_1 = require("./walkDependencyTree");
11
12
  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
- });
13
+ async function findUnusedFiles({ entryFiles, ignorePatterns = ['**/node_modules'], sourceDirectories = [], resolvers, depth, cwd = process.cwd(), }) {
14
+ cwd = await node_fs_1.default.promises.realpath(cwd);
15
+ const globFromSource = async (source) => {
16
+ const files = await fast_glob_1.default.glob(fast_glob_1.default.isDynamicPattern(source) ? source : node_path_1.default.join(source, '**'), {
17
+ dot: false,
18
+ ignore: ignorePatterns,
19
+ absolute: true,
20
+ cwd,
21
+ });
22
+ return Promise.all(files.map(async (file) => await node_fs_1.default.promises.realpath(file).catch(() => file)));
23
+ };
19
24
  const sourceDirs = sourceDirectories.length > 0 ? sourceDirectories : [cwd];
20
25
  const files = new Set([].concat(...(await Promise.all(sourceDirs.map((source) => globFromSource(source))))));
21
26
  const unvisitedFiles = new Set(files);
27
+ const visitedFiles = new Set();
22
28
  for (const entryFile of entryFiles) {
23
- const entry = node_path_1.default.resolve(cwd, entryFile);
29
+ const entry = await node_fs_1.default.promises.realpath(node_path_1.default.resolve(cwd, entryFile));
24
30
  unvisitedFiles.delete(entry);
25
31
  for await (const { source, dependency } of (0, walkDependencyTree_1.walkDependencyTree)(entry, {
26
- aliases,
32
+ resolvers,
27
33
  depth,
34
+ visited: visitedFiles,
28
35
  })) {
36
+ let resolvedDependency = dependency;
29
37
  if (files.has(dependency)) {
30
38
  debug(`${source}: ${dependency} [dependency]`);
31
39
  }
32
40
  else {
33
- debug(`${source}: ${dependency} [unknown dependency]`);
41
+ const realpath = await node_fs_1.default.promises.realpath(dependency);
42
+ if (files.has(realpath)) {
43
+ resolvedDependency = realpath;
44
+ }
45
+ else {
46
+ debug(`${source}: ${dependency} [unknown dependency]`);
47
+ }
34
48
  }
35
- unvisitedFiles.delete(dependency);
49
+ unvisitedFiles.delete(resolvedDependency);
36
50
  }
37
51
  }
38
52
  return {
39
- unusedFiles: Array.from(unvisitedFiles)
53
+ unused: Array.from(unvisitedFiles)
54
+ .map((abspath) => node_path_1.default.relative(cwd, abspath))
55
+ .sort(),
56
+ used: Array.from(visitedFiles)
40
57
  .map((abspath) => node_path_1.default.relative(cwd, abspath))
41
58
  .sort(),
42
59
  };
43
60
  }
44
- exports.findUnusedFiles = findUnusedFiles;
@@ -0,0 +1,15 @@
1
+ export interface ResolverResult {
2
+ /** Resolved path. */
3
+ result: string;
4
+ }
5
+ export interface ResolverParams {
6
+ /** The module/path being requested. */
7
+ request: string;
8
+ /** The file or directory to resolve the request from. */
9
+ context: string;
10
+ }
11
+ /**
12
+ * Used to resolve imports to the absolute path of the file.
13
+ * Return 'null' if unable to resolve and we will attempt the next resolver in the chain.
14
+ */
15
+ export type Resolver = (params: ResolverParams) => Promise<ResolverResult | null>;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,5 +1,6 @@
1
- export declare function walkDependencyTree(source: string, { aliases, visited, depth, }?: {
2
- aliases?: Partial<Record<string, string>>;
1
+ import { type Resolver } from './types';
2
+ export declare function walkDependencyTree(source: string, { resolvers, visited, depth, }?: {
3
+ resolvers?: Resolver[];
3
4
  visited?: Set<string>;
4
5
  depth?: number;
5
6
  }): AsyncGenerator<{
@@ -3,15 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.walkDependencyTree = void 0;
6
+ exports.walkDependencyTree = walkDependencyTree;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const typescript_estree_1 = require("@typescript-eslint/typescript-estree");
10
10
  const debug_1 = __importDefault(require("debug"));
11
11
  const debug = (0, debug_1.default)('unused-files:parse');
12
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, } = {}) {
13
+ const VALID_EXTENSIONS = new Set(['ts', 'tsx', 'mts', 'cts', 'js', 'jsx', 'mjs', 'cjs']);
14
+ async function* walkDependencyTree(source, { resolvers, visited, depth = DEFAULT_DEPTH_LIMIT, } = {}) {
15
15
  const ext = node_path_1.default.extname(source).substring(1);
16
16
  if (!VALID_EXTENSIONS.has(ext)) {
17
17
  debug(`${source}: Unknown file extension '${ext}' [skipping]`);
@@ -59,6 +59,11 @@ async function* walkDependencyTree(source, { aliases, visited, depth = DEFAULT_D
59
59
  importFroms.add(node.source.value);
60
60
  }
61
61
  }
62
+ else if (node.source.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.TemplateLiteral &&
63
+ !node.source.expressions.length &&
64
+ node.source.quasis.length === 1) {
65
+ importFroms.add(node.source.quasis[0].value.cooked);
66
+ }
62
67
  else {
63
68
  debug(`${source}: Dynamic import expression found at ${node.loc.start}:${node.loc.end}`);
64
69
  }
@@ -68,10 +73,12 @@ async function* walkDependencyTree(source, { aliases, visited, depth = DEFAULT_D
68
73
  for (const body of ast.body) {
69
74
  (0, typescript_estree_1.simpleTraverse)(body, { visitors });
70
75
  }
71
- const resolveToAbsPath = (request) => {
72
- const aliasedPath = aliases?.[request];
73
- if (aliasedPath) {
74
- return node_path_1.default.resolve(aliasedPath);
76
+ const resolveToAbsPath = async (request) => {
77
+ for (const resolver of resolvers ?? []) {
78
+ const resolution = await resolver({ context: source, request });
79
+ if (resolution) {
80
+ return node_path_1.default.resolve(resolution.result);
81
+ }
75
82
  }
76
83
  try {
77
84
  return require.resolve(request, { paths: [node_path_1.default.dirname(source)] });
@@ -80,12 +87,12 @@ async function* walkDependencyTree(source, { aliases, visited, depth = DEFAULT_D
80
87
  return undefined;
81
88
  };
82
89
  for (const importFrom of Array.from(importFroms)) {
83
- const absPath = resolveToAbsPath(importFrom);
90
+ const absPath = await resolveToAbsPath(importFrom);
84
91
  if (absPath) {
85
92
  yield { dependency: absPath, source };
86
93
  if (depth === -1 || depth > 0) {
87
94
  yield* walkDependencyTree(absPath, {
88
- aliases,
95
+ resolvers,
89
96
  visited: visitedSet,
90
97
  depth: depth === -1 ? depth : depth - 1,
91
98
  });
@@ -96,4 +103,3 @@ async function* walkDependencyTree(source, { aliases, visited, depth = DEFAULT_D
96
103
  }
97
104
  }
98
105
  }
99
- exports.walkDependencyTree = walkDependencyTree;
package/lib/bin.js CHANGED
@@ -5,7 +5,7 @@ const command_1 = require("./command");
5
5
  const cli = new clipanion_1.Cli({
6
6
  binaryLabel: '@noahnu/unused-files',
7
7
  binaryName: 'yarn @noahnu/unused-files',
8
- // eslint-disable-next-line @typescript-eslint/no-var-requires
8
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9
9
  binaryVersion: require('../package.json').version,
10
10
  enableCapture: true,
11
11
  });
package/lib/command.js CHANGED
@@ -57,7 +57,7 @@ class BaseCommand extends clipanion_1.Command {
57
57
  ? -1
58
58
  : parseInt(Math.max(this.depth, -1).toFixed(0), 10),
59
59
  });
60
- this.context.stdout.write(result.unusedFiles.join('\n'));
60
+ this.context.stdout.write(result.unused.join('\n'));
61
61
  }
62
62
  }
63
63
  exports.BaseCommand = BaseCommand;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noahnu/unused-files",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/noahnu/nodejs-tools.git",
@@ -15,7 +15,7 @@
15
15
  "scripts": {
16
16
  "clean": "run workspace:clean \"$(pwd)\"",
17
17
  "prepack": "run workspace:build \"$(pwd)\"",
18
- "run-local": "run -T ts-node --transpileOnly ./src/bin.ts"
18
+ "run-local": "run -T tsx ./src/bin.ts"
19
19
  },
20
20
  "bin": "./lib/bin.js",
21
21
  "main": "./lib/api/index.js",
@@ -31,16 +31,17 @@
31
31
  ],
32
32
  "dependencies": {
33
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",
34
+ "@typescript-eslint/typescript-estree": "^8.13.0",
35
+ "clipanion": "^4.0.0-rc.4",
36
+ "debug": "^4.3.7",
37
37
  "fast-glob": "^3.3.2",
38
38
  "typanion": "^3.14.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@jest/globals": "^29.7.0",
42
42
  "@noahnu/internal-test-utils": "0.0.0",
43
- "@types/node": "^20.9.0"
43
+ "@types/node": "^22.9.0",
44
+ "typescript": "^5.6.3"
44
45
  },
45
46
  "types": "./lib/api/index.d.ts"
46
47
  }