@noahnu/unused-files 0.0.1 → 0.1.0

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 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
  }