@noahnu/unused-files 0.2.2 → 0.3.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
@@ -2,6 +2,18 @@
2
2
 
3
3
  <!-- MONOWEAVE:BELOW -->
4
4
 
5
+ ## @noahnu/unused-files (v0.3.0) <a name="0.3.0"></a>
6
+
7
+ Migrate to ESM.
8
+
9
+
10
+
11
+ ## @noahnu/unused-files (v0.2.3) <a name="0.2.3"></a>
12
+
13
+ Ignore builtins.
14
+
15
+
16
+
5
17
  ## @noahnu/unused-files (v0.2.2) <a name="0.2.2"></a>
6
18
 
7
19
  Filter out files based on source directories.
@@ -1,5 +1,5 @@
1
- import { type Resolver } from './types';
2
- export type { Resolver, ResolverResult, ResolverParams } from './types';
1
+ import { type Resolver } from '@noahnu/dependency-utils';
2
+ export type { Resolver, ResolverResult, ResolverParams } from '@noahnu/dependency-utils';
3
3
  export interface FindUnusedFilesOptions {
4
4
  /**
5
5
  * Entry files into the codebase. These files are known to be used and any files that
@@ -0,0 +1,57 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveRealpath, walkDependencyTree } from '@noahnu/dependency-utils';
4
+ import createDebug from 'debug';
5
+ import fg from 'fast-glob';
6
+ const debug = createDebug('unused-files');
7
+ export async function findUnusedFiles({ entryFiles, ignorePatterns = ['**/node_modules'], sourceDirectories = [], resolvers, depth, cwd = process.cwd(), }) {
8
+ cwd = await fs.promises.realpath(cwd);
9
+ const globFromSource = async (source) => {
10
+ const files = await fg.glob(fg.isDynamicPattern(source) ? source : path.join(source, '**'), {
11
+ dot: false,
12
+ ignore: ignorePatterns,
13
+ absolute: true,
14
+ cwd,
15
+ });
16
+ return Promise.all(files.map(async (file) => await resolveRealpath(file)));
17
+ };
18
+ const sourceDirs = sourceDirectories.length > 0 ? sourceDirectories : [cwd];
19
+ const files = new Set([].concat(...(await Promise.all(sourceDirs.map((source) => globFromSource(source))))));
20
+ const unvisitedFiles = new Set(files);
21
+ const visitedFiles = new Set();
22
+ for (const entryFile of entryFiles) {
23
+ const entry = await resolveRealpath(path.resolve(cwd, entryFile));
24
+ unvisitedFiles.delete(entry);
25
+ for await (const { source, dependency } of walkDependencyTree(entry, {
26
+ resolvers,
27
+ depth,
28
+ visited: visitedFiles,
29
+ ignorePatterns,
30
+ })) {
31
+ let resolvedDependency = dependency;
32
+ if (files.has(dependency)) {
33
+ debug(`${source}: ${dependency} [dependency]`);
34
+ }
35
+ else {
36
+ const realpath = await resolveRealpath(dependency);
37
+ if (files.has(realpath)) {
38
+ resolvedDependency = realpath;
39
+ }
40
+ else {
41
+ debug(`${source}: ${dependency} [unknown dependency]`);
42
+ }
43
+ }
44
+ unvisitedFiles.delete(resolvedDependency);
45
+ }
46
+ }
47
+ const unused = Array.from(unvisitedFiles.intersection(files))
48
+ .map((abspath) => path.relative(cwd, abspath))
49
+ .sort();
50
+ const used = Array.from(visitedFiles.intersection(files))
51
+ .map((abspath) => path.relative(cwd, abspath))
52
+ .sort();
53
+ return {
54
+ unused,
55
+ used,
56
+ };
57
+ }
@@ -1,13 +1,11 @@
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({
1
+ import { Cli } from 'clipanion';
2
+ import { BaseCommand } from './command.mjs';
3
+ const cli = new Cli({
6
4
  binaryLabel: '@noahnu/unused-files',
7
5
  binaryName: 'yarn @noahnu/unused-files',
8
6
  // eslint-disable-next-line @typescript-eslint/no-require-imports
9
7
  binaryVersion: require('../package.json').version,
10
8
  enableCapture: true,
11
9
  });
12
- cli.register(command_1.BaseCommand);
10
+ cli.register(BaseCommand);
13
11
  cli.runExit(process.argv.slice(2));
@@ -0,0 +1,36 @@
1
+ import { Command, Option } from 'clipanion';
2
+ import * as t from 'typanion';
3
+ import { findUnusedFiles } from './api/index.mjs';
4
+ export class BaseCommand extends Command {
5
+ entryFiles = Option.Array('--entry', {
6
+ description: 'Entry files into the codebase. These files are known to be ' +
7
+ 'used and any files that are dependencies of these entry files ' +
8
+ 'are also considered used files.',
9
+ required: true,
10
+ });
11
+ ignorePatterns = Option.Array('--ignore', {
12
+ description: 'Glob patterns usued to exclude files. ' +
13
+ 'The patterns are applied during traversal ' +
14
+ 'of the directory tree.',
15
+ required: false,
16
+ });
17
+ depth = Option.String('--depth', {
18
+ description: 'Depth limit. Set to -1 to disable.',
19
+ required: false,
20
+ validator: t.isNumber(),
21
+ });
22
+ sourceDirectories = Option.Rest();
23
+ async execute() {
24
+ const result = await findUnusedFiles({
25
+ entryFiles: this.entryFiles,
26
+ ignorePatterns: this.ignorePatterns,
27
+ sourceDirectories: this.sourceDirectories.length
28
+ ? this.sourceDirectories
29
+ : [process.cwd()],
30
+ depth: typeof this.depth === 'undefined'
31
+ ? -1
32
+ : parseInt(Math.max(this.depth, -1).toFixed(0), 10),
33
+ });
34
+ this.context.stdout.write(result.unused.join('\n'));
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noahnu/unused-files",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/noahnu/nodejs-tools.git",
@@ -15,26 +15,27 @@
15
15
  "scripts": {
16
16
  "clean": "run workspace:clean \"$(pwd)\"",
17
17
  "prepack": "run workspace:build \"$(pwd)\"",
18
- "run-local": "run -T tsx ./src/bin.ts"
18
+ "run-local": "run -T tsx ./src/bin.mts"
19
19
  },
20
- "bin": "./lib/bin.js",
21
- "main": "./lib/api/index.js",
20
+ "bin": "./lib/bin.mjs",
21
+ "main": "./lib/api/index.mjs",
22
22
  "publishConfig": {
23
23
  "registry": "https://registry.npmjs.org/",
24
24
  "access": "public",
25
- "bin": "./lib/bin.js",
26
- "main": "./lib/api/index.js",
27
- "types": "./lib/api/index.d.ts"
25
+ "bin": "./lib/bin.mjs",
26
+ "main": "./lib/api/index.mjs",
27
+ "types": "./lib/api/index.d.mts"
28
28
  },
29
29
  "files": [
30
30
  "lib"
31
31
  ],
32
32
  "dependencies": {
33
+ "@noahnu/dependency-utils": "^0.0.1",
33
34
  "@types/debug": "^4.1.12",
34
- "@typescript-eslint/typescript-estree": "^8.13.0",
35
+ "@typescript-eslint/typescript-estree": "^8.22.0",
35
36
  "clipanion": "^4.0.0-rc.4",
36
- "debug": "^4.3.7",
37
- "fast-glob": "^3.3.2",
37
+ "debug": "^4.4.0",
38
+ "fast-glob": "^3.3.3",
38
39
  "micromatch": "^4.0.8",
39
40
  "typanion": "^3.14.0"
40
41
  },
@@ -42,8 +43,8 @@
42
43
  "@jest/globals": "^29.7.0",
43
44
  "@noahnu/internal-test-utils": "0.0.0",
44
45
  "@types/micromatch": "^4.0.9",
45
- "@types/node": "^22.9.0",
46
- "typescript": "^5.7.2"
46
+ "@types/node": "^22.12.0",
47
+ "typescript": "^5.7.3"
47
48
  },
48
- "types": "./lib/api/index.d.ts"
49
+ "types": "./lib/api/index.d.mts"
49
50
  }
package/lib/api/index.js DELETED
@@ -1,63 +0,0 @@
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 = findUnusedFiles;
7
- const node_fs_1 = __importDefault(require("node:fs"));
8
- const node_path_1 = __importDefault(require("node:path"));
9
- const debug_1 = __importDefault(require("debug"));
10
- const fast_glob_1 = __importDefault(require("fast-glob"));
11
- const walkDependencyTree_1 = require("./walkDependencyTree");
12
- const debug = (0, debug_1.default)('unused-files');
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
- };
24
- const sourceDirs = sourceDirectories.length > 0 ? sourceDirectories : [cwd];
25
- const files = new Set([].concat(...(await Promise.all(sourceDirs.map((source) => globFromSource(source))))));
26
- const unvisitedFiles = new Set(files);
27
- const visitedFiles = new Set();
28
- for (const entryFile of entryFiles) {
29
- const entry = await node_fs_1.default.promises.realpath(node_path_1.default.resolve(cwd, entryFile));
30
- unvisitedFiles.delete(entry);
31
- for await (const { source, dependency } of (0, walkDependencyTree_1.walkDependencyTree)(entry, {
32
- resolvers,
33
- depth,
34
- visited: visitedFiles,
35
- ignorePatterns,
36
- })) {
37
- let resolvedDependency = dependency;
38
- if (files.has(dependency)) {
39
- debug(`${source}: ${dependency} [dependency]`);
40
- }
41
- else {
42
- const realpath = await node_fs_1.default.promises.realpath(dependency);
43
- if (files.has(realpath)) {
44
- resolvedDependency = realpath;
45
- }
46
- else {
47
- debug(`${source}: ${dependency} [unknown dependency]`);
48
- }
49
- }
50
- unvisitedFiles.delete(resolvedDependency);
51
- }
52
- }
53
- const unused = Array.from(unvisitedFiles.intersection(files))
54
- .map((abspath) => node_path_1.default.relative(cwd, abspath))
55
- .sort();
56
- const used = Array.from(visitedFiles.intersection(files))
57
- .map((abspath) => node_path_1.default.relative(cwd, abspath))
58
- .sort();
59
- return {
60
- unused,
61
- used,
62
- };
63
- }
@@ -1,15 +0,0 @@
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>;
package/lib/api/types.js DELETED
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,10 +0,0 @@
1
- import { type Resolver } from './types';
2
- export declare function walkDependencyTree(source: string, { resolvers, visited, depth, ignorePatterns, }?: {
3
- resolvers?: Resolver[];
4
- visited?: Set<string>;
5
- depth?: number;
6
- ignorePatterns?: string[];
7
- }): AsyncGenerator<{
8
- source: string;
9
- dependency: string;
10
- }, void, void>;
@@ -1,112 +0,0 @@
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 = walkDependencyTree;
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 micromatch_1 = __importDefault(require("micromatch"));
12
- const debug = (0, debug_1.default)('unused-files:parse');
13
- const DEFAULT_DEPTH_LIMIT = -1; // no depth limit
14
- const VALID_EXTENSIONS = new Set(['ts', 'tsx', 'mts', 'cts', 'js', 'jsx', 'mjs', 'cjs']);
15
- async function* walkDependencyTree(source, { resolvers, visited, depth = DEFAULT_DEPTH_LIMIT, ignorePatterns, } = {}) {
16
- const ext = node_path_1.default.extname(source).substring(1);
17
- if (!VALID_EXTENSIONS.has(ext)) {
18
- debug(`${source}: Unknown file extension '${ext}' [skipping]`);
19
- return;
20
- }
21
- // Convert to realpath if possible
22
- source = await node_fs_1.default.promises.realpath(source).catch(() => source);
23
- const visitedSet = visited ?? new Set();
24
- if (visitedSet.has(source))
25
- return;
26
- visitedSet.add(source);
27
- const code = await node_fs_1.default.promises.readFile(source, { encoding: 'utf-8' });
28
- const ast = (0, typescript_estree_1.parse)(code, {
29
- allowInvalidAST: true,
30
- comment: false,
31
- suppressDeprecatedPropertyWarnings: true,
32
- errorOnUnknownASTType: false,
33
- filePath: source,
34
- jsDocParsingMode: 'none',
35
- });
36
- const importFroms = new Set();
37
- const visitors = {
38
- [typescript_estree_1.TSESTree.AST_NODE_TYPES.ImportDeclaration]: (node) => {
39
- if (node.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.ImportDeclaration) {
40
- importFroms.add(node.source.value);
41
- }
42
- },
43
- [typescript_estree_1.TSESTree.AST_NODE_TYPES.CallExpression]: (node) => {
44
- if (node.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.CallExpression &&
45
- node.callee.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.Identifier &&
46
- node.callee.name === 'require') {
47
- const arg = node.arguments[0];
48
- if (arg.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.Literal) {
49
- if (typeof arg.value === 'string') {
50
- importFroms.add(arg.value);
51
- }
52
- }
53
- else {
54
- debug(`${source}: Dynamic require expression found at ${node.loc.start}:${node.loc.end}`);
55
- }
56
- }
57
- },
58
- [typescript_estree_1.TSESTree.AST_NODE_TYPES.ImportExpression]: (node) => {
59
- if (node.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.ImportExpression) {
60
- if (node.source.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.Literal) {
61
- if (typeof node.source.value === 'string') {
62
- importFroms.add(node.source.value);
63
- }
64
- }
65
- else if (node.source.type === typescript_estree_1.TSESTree.AST_NODE_TYPES.TemplateLiteral &&
66
- !node.source.expressions.length &&
67
- node.source.quasis.length === 1) {
68
- importFroms.add(node.source.quasis[0].value.cooked);
69
- }
70
- else {
71
- debug(`${source}: Dynamic import expression found at ${node.loc.start}:${node.loc.end}`);
72
- }
73
- }
74
- },
75
- };
76
- for (const body of ast.body) {
77
- (0, typescript_estree_1.simpleTraverse)(body, { visitors });
78
- }
79
- const resolveToAbsPath = async (request) => {
80
- for (const resolver of resolvers ?? []) {
81
- const resolution = await resolver({ context: source, request });
82
- if (resolution) {
83
- return node_path_1.default.resolve(resolution.result);
84
- }
85
- }
86
- try {
87
- return require.resolve(request, { paths: [node_path_1.default.dirname(source)] });
88
- }
89
- catch { }
90
- return undefined;
91
- };
92
- for (const importFrom of Array.from(importFroms)) {
93
- const absPath = await resolveToAbsPath(importFrom);
94
- if (absPath) {
95
- if (ignorePatterns && micromatch_1.default.isMatch(absPath, ignorePatterns)) {
96
- continue;
97
- }
98
- yield { dependency: absPath, source };
99
- if (depth === -1 || depth > 0) {
100
- yield* walkDependencyTree(absPath, {
101
- resolvers,
102
- visited: visitedSet,
103
- depth: depth === -1 ? depth : depth - 1,
104
- ignorePatterns,
105
- });
106
- }
107
- }
108
- else {
109
- debug(`${source}: Unable to resolve '${importFrom}'`);
110
- }
111
- }
112
- }
package/lib/command.js DELETED
@@ -1,73 +0,0 @@
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 () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.BaseCommand = void 0;
37
- const clipanion_1 = require("clipanion");
38
- const t = __importStar(require("typanion"));
39
- const api_1 = require("./api");
40
- class BaseCommand extends clipanion_1.Command {
41
- entryFiles = clipanion_1.Option.Array('--entry', {
42
- description: 'Entry files into the codebase. These files are known to be ' +
43
- 'used and any files that are dependencies of these entry files ' +
44
- 'are also considered used files.',
45
- required: true,
46
- });
47
- ignorePatterns = clipanion_1.Option.Array('--ignore', {
48
- description: 'Glob patterns usued to exclude files. ' +
49
- 'The patterns are applied during traversal ' +
50
- 'of the directory tree.',
51
- required: false,
52
- });
53
- depth = clipanion_1.Option.String('--depth', {
54
- description: 'Depth limit. Set to -1 to disable.',
55
- required: false,
56
- validator: t.isNumber(),
57
- });
58
- sourceDirectories = clipanion_1.Option.Rest();
59
- async execute() {
60
- const result = await (0, api_1.findUnusedFiles)({
61
- entryFiles: this.entryFiles,
62
- ignorePatterns: this.ignorePatterns,
63
- sourceDirectories: this.sourceDirectories.length
64
- ? this.sourceDirectories
65
- : [process.cwd()],
66
- depth: typeof this.depth === 'undefined'
67
- ? -1
68
- : parseInt(Math.max(this.depth, -1).toFixed(0), 10),
69
- });
70
- this.context.stdout.write(result.unused.join('\n'));
71
- }
72
- }
73
- exports.BaseCommand = BaseCommand;
File without changes
File without changes