importy 0.0.6 → 0.0.7
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/README.md +170 -0
- package/dist/index.js +266 -48
- package/package.json +18 -4
- package/.github/workflows/publish.yml +0 -36
- package/src/index.ts +0 -105
- package/tsconfig.json +0 -14
- package/tsup.config.ts +0 -12
package/README.md
CHANGED
|
@@ -1 +1,171 @@
|
|
|
1
1
|
# Importy
|
|
2
|
+
|
|
3
|
+
A powerful CLI tool for analyzing JavaScript/TypeScript imports from libraries.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/importy)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://github.com/yourusername/importy)
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Importy scans your codebase to identify and analyze imports from specific libraries. It helps you:
|
|
12
|
+
|
|
13
|
+
- Identify which components from a library are being used in your codebase
|
|
14
|
+
- Find all occurrences of specific imported components
|
|
15
|
+
- Analyze library usage patterns across your project
|
|
16
|
+
- Generate detailed reports for dependency management
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Using npm
|
|
22
|
+
npm install -g importy
|
|
23
|
+
|
|
24
|
+
# Using yarn
|
|
25
|
+
yarn global add importy
|
|
26
|
+
|
|
27
|
+
# Using pnpm
|
|
28
|
+
pnpm add -g importy
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
importy --dir <directory> --lib <library-name> [options]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Required Options
|
|
38
|
+
|
|
39
|
+
- `-d, --dir <directory>`: Directory to scan (required)
|
|
40
|
+
- `-l, --lib <library>`: Library name to match (required)
|
|
41
|
+
|
|
42
|
+
### Additional Options
|
|
43
|
+
|
|
44
|
+
- `-o, --output <file>`: Output results to a JSON file instead of stdout
|
|
45
|
+
- `-v, --verbose`: Enable verbose logging
|
|
46
|
+
- `-i, --include <pattern>`: Only include files matching pattern (glob)
|
|
47
|
+
- `-e, --exclude <pattern>`: Exclude files matching pattern (glob)
|
|
48
|
+
- `-c, --concurrency <number>`: Number of worker threads (defaults to CPU count - 1)
|
|
49
|
+
- `--version`: Show version number
|
|
50
|
+
- `--help`: Show help
|
|
51
|
+
|
|
52
|
+
## Examples
|
|
53
|
+
|
|
54
|
+
### Basic Usage
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Find all React imports in src directory
|
|
58
|
+
importy --dir ./src --lib react
|
|
59
|
+
|
|
60
|
+
# Find all MUI components used in your project
|
|
61
|
+
importy --dir ./src --lib @mui/material
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Advanced Usage
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Export results to a JSON file
|
|
68
|
+
importy --dir ./src --lib lodash --output imports.json
|
|
69
|
+
|
|
70
|
+
# Only scan TypeScript files
|
|
71
|
+
importy --dir ./src --lib axios --include "**/*.ts"
|
|
72
|
+
|
|
73
|
+
# Exclude test files
|
|
74
|
+
importy --dir ./src --lib react --exclude "**/*.test.{ts,tsx}"
|
|
75
|
+
|
|
76
|
+
# Limit concurrency
|
|
77
|
+
importy --dir ./src --lib react --concurrency 4
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Output Format
|
|
81
|
+
|
|
82
|
+
The tool outputs JSON in the following format:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"summary": {
|
|
87
|
+
"library": "react",
|
|
88
|
+
"componentsFound": 5,
|
|
89
|
+
"totalImports": 12,
|
|
90
|
+
"filesScanned": 42
|
|
91
|
+
},
|
|
92
|
+
"components": {
|
|
93
|
+
"useState": [
|
|
94
|
+
"src/components/Counter.tsx",
|
|
95
|
+
"src/components/Form.tsx"
|
|
96
|
+
],
|
|
97
|
+
"useEffect": [
|
|
98
|
+
"src/components/Dashboard.tsx"
|
|
99
|
+
],
|
|
100
|
+
"Component": [
|
|
101
|
+
"src/components/BaseComponent.tsx"
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Performance
|
|
108
|
+
|
|
109
|
+
Importy uses parallel processing with promises, making it efficient even for large codebases. You can adjust the concurrency level to match your system's capabilities using the `--concurrency` option.
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
### Prerequisites
|
|
114
|
+
|
|
115
|
+
- Node.js 16+
|
|
116
|
+
- npm, yarn, or pnpm
|
|
117
|
+
|
|
118
|
+
### Setup
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Clone the repository
|
|
122
|
+
git clone https://github.com/yourusername/importy.git
|
|
123
|
+
cd importy
|
|
124
|
+
|
|
125
|
+
# Install dependencies
|
|
126
|
+
npm install
|
|
127
|
+
|
|
128
|
+
# Build the project
|
|
129
|
+
npm run build
|
|
130
|
+
|
|
131
|
+
# Run tests
|
|
132
|
+
npm test
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Testing
|
|
136
|
+
|
|
137
|
+
Importy uses Vitest for testing. There are two types of tests:
|
|
138
|
+
|
|
139
|
+
1. **Programmatic tests**: Test the core functionality through the JavaScript API
|
|
140
|
+
2. **CLI tests**: Test the command-line interface
|
|
141
|
+
|
|
142
|
+
Run tests with:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Run all tests
|
|
146
|
+
npm test
|
|
147
|
+
|
|
148
|
+
# Run specific tests
|
|
149
|
+
npx vitest run tests/programmatic.test.ts
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Contributing
|
|
153
|
+
|
|
154
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
155
|
+
|
|
156
|
+
1. Fork the repository
|
|
157
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
158
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
159
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
160
|
+
5. Open a Pull Request
|
|
161
|
+
|
|
162
|
+
## Troubleshooting
|
|
163
|
+
|
|
164
|
+
### Common Issues
|
|
165
|
+
|
|
166
|
+
- **ES Module Compatibility**: If you encounter issues with ES modules, ensure your Node.js version is compatible (16+) and you're using the correct import syntax.
|
|
167
|
+
- **Parsing Errors**: Complex TypeScript/JSX syntax may occasionally cause parsing errors. These files are skipped with a warning.
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
package/dist/index.js
CHANGED
|
@@ -1,74 +1,292 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import fs2 from "fs";
|
|
5
|
+
import path2 from "path";
|
|
6
|
+
import { program } from "commander";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { dirname } from "path";
|
|
9
|
+
|
|
10
|
+
// src/cli.ts
|
|
4
11
|
import fs from "fs";
|
|
5
12
|
import path from "path";
|
|
6
|
-
import { program } from "commander";
|
|
7
13
|
import * as parser from "@babel/parser";
|
|
8
14
|
import _traverse from "@babel/traverse";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
var { dir, lib } = program.opts();
|
|
15
|
+
import os from "os";
|
|
16
|
+
var traverse = _traverse.default;
|
|
12
17
|
function isJavaScriptFile(file) {
|
|
13
18
|
return /\.(js|ts|jsx|tsx)$/.test(file);
|
|
14
19
|
}
|
|
15
|
-
function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
function getAllFiles(dirPath, arrayOfFiles = [], includePattern, excludePattern, verbose = false) {
|
|
21
|
+
try {
|
|
22
|
+
const files = fs.readdirSync(dirPath);
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
const fullPath = path.join(dirPath, file);
|
|
25
|
+
try {
|
|
26
|
+
const stat = fs.statSync(fullPath);
|
|
27
|
+
if (excludePattern && stat.isDirectory() && minimatch(fullPath, excludePattern)) {
|
|
28
|
+
if (verbose) console.log(`Skipping excluded directory: ${fullPath}`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (stat.isDirectory()) {
|
|
32
|
+
getAllFiles(fullPath, arrayOfFiles, includePattern, excludePattern, verbose);
|
|
33
|
+
} else if (isJavaScriptFile(fullPath)) {
|
|
34
|
+
const shouldInclude = !includePattern || minimatch(fullPath, includePattern);
|
|
35
|
+
const shouldExclude = excludePattern && minimatch(fullPath, excludePattern);
|
|
36
|
+
if (shouldInclude && !shouldExclude) {
|
|
37
|
+
arrayOfFiles.push(fullPath);
|
|
38
|
+
} else if (verbose) {
|
|
39
|
+
console.log(`Skipping file due to patterns: ${fullPath}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (verbose) console.warn(`Error accessing file ${fullPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
44
|
+
}
|
|
24
45
|
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (verbose) console.error(`Error reading directory ${dirPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
25
48
|
}
|
|
26
49
|
return arrayOfFiles;
|
|
27
50
|
}
|
|
51
|
+
function minimatch(filePath, pattern) {
|
|
52
|
+
try {
|
|
53
|
+
const regExpPattern = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/{{GLOBSTAR}}/g, ".*");
|
|
54
|
+
const regex = new RegExp(`^${regExpPattern}$`, "i");
|
|
55
|
+
const result = regex.test(filePath);
|
|
56
|
+
return result;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn(`Invalid pattern: ${pattern}`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function chunkArray(array, chunkSize) {
|
|
63
|
+
const result = [];
|
|
64
|
+
for (let i = 0; i < array.length; i += chunkSize) {
|
|
65
|
+
result.push(array.slice(i, i + chunkSize));
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
async function processFilesInBatches(items, batchSize, processor, onProgress) {
|
|
70
|
+
const results = [];
|
|
71
|
+
const batches = chunkArray(items, batchSize);
|
|
72
|
+
let completed = 0;
|
|
73
|
+
const total = items.length;
|
|
74
|
+
for (const batch of batches) {
|
|
75
|
+
const batchResults = await Promise.all(
|
|
76
|
+
batch.map(async (item) => {
|
|
77
|
+
try {
|
|
78
|
+
const result = await processor(item);
|
|
79
|
+
completed++;
|
|
80
|
+
if (onProgress) {
|
|
81
|
+
onProgress(completed, total);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.warn(`Error processing item: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
completed++;
|
|
87
|
+
if (onProgress) {
|
|
88
|
+
onProgress(completed, total);
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
results.push(...batchResults);
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
28
98
|
function extractImportsFromFile(filePath, targetLib) {
|
|
29
|
-
const code = fs.readFileSync(filePath, "utf-8");
|
|
30
|
-
let ast;
|
|
31
99
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
100
|
+
const code = fs.readFileSync(filePath, "utf-8");
|
|
101
|
+
let ast;
|
|
102
|
+
try {
|
|
103
|
+
ast = parser.parse(code, {
|
|
104
|
+
sourceType: "module",
|
|
105
|
+
plugins: ["typescript", "jsx"],
|
|
106
|
+
errorRecovery: true
|
|
107
|
+
});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
try {
|
|
110
|
+
ast = parser.parse(code, {
|
|
111
|
+
sourceType: "module",
|
|
112
|
+
plugins: ["typescript", "jsx", "decorators-legacy", "classProperties"],
|
|
113
|
+
errorRecovery: true
|
|
114
|
+
});
|
|
115
|
+
} catch (secondErr) {
|
|
116
|
+
console.warn(`Skipping ${filePath}: Failed to parse: ${secondErr}`);
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const matches = [];
|
|
121
|
+
const lines = code.split("\n");
|
|
122
|
+
traverse(ast, {
|
|
123
|
+
ImportDeclaration(path3) {
|
|
124
|
+
const node = path3.node;
|
|
125
|
+
if (node.source.value === targetLib || // Handle subpath imports like 'library/subpath'
|
|
126
|
+
node.source.value.startsWith(`${targetLib}/`)) {
|
|
127
|
+
const lineNumber = node.loc?.start.line;
|
|
128
|
+
for (const specifier of node.specifiers) {
|
|
129
|
+
let importedName;
|
|
130
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
131
|
+
importedName = "default";
|
|
132
|
+
} else if (specifier.type === "ImportNamespaceSpecifier") {
|
|
133
|
+
importedName = "*";
|
|
134
|
+
} else if ("imported" in specifier && specifier.imported) {
|
|
135
|
+
importedName = specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value);
|
|
136
|
+
} else {
|
|
137
|
+
importedName = "unknown";
|
|
138
|
+
}
|
|
139
|
+
const localName = specifier.local.name;
|
|
140
|
+
matches.push({
|
|
141
|
+
importedName,
|
|
142
|
+
localName,
|
|
143
|
+
file: filePath,
|
|
144
|
+
line: lineNumber
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
35
149
|
});
|
|
36
|
-
|
|
37
|
-
|
|
150
|
+
return matches;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.warn(`Error reading file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
153
|
return [];
|
|
39
154
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
155
|
+
}
|
|
156
|
+
async function analyzeImports(options2) {
|
|
157
|
+
const { dir, lib, include, exclude, verbose = false } = options2;
|
|
158
|
+
const concurrency = options2.concurrency || Math.max(1, Math.min(4, os.cpus().length - 1));
|
|
159
|
+
if (!fs.existsSync(dir)) {
|
|
160
|
+
throw new Error(`Directory '${dir}' does not exist`);
|
|
161
|
+
}
|
|
162
|
+
if (!fs.statSync(dir).isDirectory()) {
|
|
163
|
+
throw new Error(`'${dir}' is not a directory`);
|
|
164
|
+
}
|
|
165
|
+
const allFiles = getAllFiles(dir, [], include, exclude, verbose);
|
|
166
|
+
if (verbose) console.log(`Found ${allFiles.length} files to process`);
|
|
167
|
+
if (allFiles.length === 0) {
|
|
168
|
+
return {
|
|
169
|
+
summary: {
|
|
170
|
+
library: lib,
|
|
171
|
+
componentsFound: 0,
|
|
172
|
+
totalImports: 0,
|
|
173
|
+
filesScanned: 0
|
|
174
|
+
},
|
|
175
|
+
components: {}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const batchSize = Math.max(1, Math.ceil(allFiles.length / concurrency));
|
|
179
|
+
if (verbose) console.log(`Processing ${allFiles.length} files with ${concurrency} concurrent processes`);
|
|
180
|
+
let processedCount = 0;
|
|
181
|
+
const results = await processFilesInBatches(
|
|
182
|
+
allFiles,
|
|
183
|
+
batchSize,
|
|
184
|
+
(file) => extractImportsFromFile(file, lib),
|
|
185
|
+
(completed, total) => {
|
|
186
|
+
if (verbose) {
|
|
187
|
+
const percentComplete = Math.floor(completed / total * 100);
|
|
188
|
+
if (percentComplete % 10 === 0 && processedCount !== percentComplete) {
|
|
189
|
+
processedCount = percentComplete;
|
|
190
|
+
console.log(`Progress: ${percentComplete}% (${completed}/${total} files)`);
|
|
53
191
|
}
|
|
54
192
|
}
|
|
55
193
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
for (const file of allFiles) {
|
|
62
|
-
const imports = extractImportsFromFile(file, lib);
|
|
63
|
-
for (const { importedName, file: filePath } of imports) {
|
|
64
|
-
if (!componentMap[importedName]) {
|
|
65
|
-
componentMap[importedName] = /* @__PURE__ */ new Set();
|
|
194
|
+
);
|
|
195
|
+
const componentMap = {};
|
|
196
|
+
for (const imports of results) {
|
|
197
|
+
if (!imports) {
|
|
198
|
+
continue;
|
|
66
199
|
}
|
|
67
|
-
|
|
200
|
+
for (const { importedName, file: filePath } of imports) {
|
|
201
|
+
if (!componentMap[importedName]) {
|
|
202
|
+
componentMap[importedName] = /* @__PURE__ */ new Set();
|
|
203
|
+
}
|
|
204
|
+
componentMap[importedName].add(filePath);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const output = {};
|
|
208
|
+
for (const [component, files] of Object.entries(componentMap)) {
|
|
209
|
+
output[component] = [...files];
|
|
210
|
+
}
|
|
211
|
+
const totalImports = Object.values(output).reduce((sum, files) => sum + files.length, 0);
|
|
212
|
+
const totalComponents = Object.keys(output).length;
|
|
213
|
+
if (verbose) {
|
|
214
|
+
console.log(`Analysis complete - Found ${totalComponents} components with ${totalImports} total imports across ${allFiles.length} files`);
|
|
68
215
|
}
|
|
216
|
+
return {
|
|
217
|
+
summary: {
|
|
218
|
+
library: lib,
|
|
219
|
+
componentsFound: totalComponents,
|
|
220
|
+
totalImports,
|
|
221
|
+
filesScanned: allFiles.length
|
|
222
|
+
},
|
|
223
|
+
components: output
|
|
224
|
+
};
|
|
69
225
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
226
|
+
|
|
227
|
+
// src/index.ts
|
|
228
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
229
|
+
var __dirname = dirname(__filename);
|
|
230
|
+
var packageJson;
|
|
231
|
+
try {
|
|
232
|
+
const packagePath = path2.resolve(__dirname, "../package.json");
|
|
233
|
+
const packageData = fs2.readFileSync(packagePath, "utf8");
|
|
234
|
+
packageJson = JSON.parse(packageData);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
packageJson = { version: "0.0.0", name: "importy" };
|
|
237
|
+
console.warn("Could not load package.json, using default version");
|
|
238
|
+
}
|
|
239
|
+
program.version(packageJson.version).description("Analyze JavaScript/TypeScript imports from a specific library").requiredOption("-d, --dir <directory>", "Directory to scan").requiredOption("-l, --lib <library>", "Library name to match").option(
|
|
240
|
+
"-o, --output <file>",
|
|
241
|
+
"Output results to a JSON file instead of stdout"
|
|
242
|
+
).option("-v, --verbose", "Enable verbose logging").option(
|
|
243
|
+
"-i, --include <pattern>",
|
|
244
|
+
"Only include files matching pattern (glob)"
|
|
245
|
+
).option("-e, --exclude <pattern>", "Exclude files matching pattern (glob)").option(
|
|
246
|
+
"-c, --concurrency <number>",
|
|
247
|
+
"Number of worker threads (defaults to CPU count - 1)"
|
|
248
|
+
).parse(process.argv);
|
|
249
|
+
var options = program.opts();
|
|
250
|
+
async function main() {
|
|
251
|
+
try {
|
|
252
|
+
const result = await analyzeImports({
|
|
253
|
+
dir: options.dir,
|
|
254
|
+
lib: options.lib,
|
|
255
|
+
include: options.include,
|
|
256
|
+
exclude: options.exclude,
|
|
257
|
+
verbose: options.verbose || false,
|
|
258
|
+
concurrency: options.concurrency ? parseInt(options.concurrency, 10) : void 0
|
|
259
|
+
});
|
|
260
|
+
if (options.output) {
|
|
261
|
+
try {
|
|
262
|
+
fs2.writeFileSync(options.output, JSON.stringify(result, null, 2));
|
|
263
|
+
console.log(`Results written to ${options.output}`);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error(
|
|
266
|
+
`Error writing to output file: ${error instanceof Error ? error.message : String(error)}`
|
|
267
|
+
);
|
|
268
|
+
console.log(JSON.stringify(result, null, 2));
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
console.log(JSON.stringify(result, null, 2));
|
|
272
|
+
}
|
|
273
|
+
if (Object.keys(result.components).length === 0) {
|
|
274
|
+
console.warn(
|
|
275
|
+
`No imports from '${options.lib}' were found in the specified directory.`
|
|
276
|
+
);
|
|
277
|
+
process.exit(0);
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(
|
|
282
|
+
`Error during processing: ${error instanceof Error ? error.message : String(error)}`
|
|
283
|
+
);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
73
286
|
}
|
|
74
|
-
|
|
287
|
+
main().catch((error) => {
|
|
288
|
+
console.error(
|
|
289
|
+
`Unhandled error: ${error instanceof Error ? error.message : String(error)}`
|
|
290
|
+
);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "importy",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "A CLI tool for analyzing JavaScript/TypeScript imports from libraries",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -13,11 +13,20 @@
|
|
|
13
13
|
"publishConfig": {
|
|
14
14
|
"registry": "https://registry.npmjs.org/"
|
|
15
15
|
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
16
19
|
"devDependencies": {
|
|
17
20
|
"@babel/types": "^7.27.3",
|
|
21
|
+
"@types/babel__traverse": "^7.20.7",
|
|
18
22
|
"@types/node": "^22.15.29",
|
|
23
|
+
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
|
24
|
+
"@typescript-eslint/parser": "^7.4.0",
|
|
25
|
+
"eslint": "^8.57.0",
|
|
26
|
+
"ts-node": "^10.9.2",
|
|
19
27
|
"tsup": "^8.5.0",
|
|
20
|
-
"typescript": "^5.8.3"
|
|
28
|
+
"typescript": "^5.8.3",
|
|
29
|
+
"vitest": "^1.4.0"
|
|
21
30
|
},
|
|
22
31
|
"dependencies": {
|
|
23
32
|
"@babel/parser": "^7.27.5",
|
|
@@ -25,6 +34,11 @@
|
|
|
25
34
|
"commander": "^14.0.0"
|
|
26
35
|
},
|
|
27
36
|
"scripts": {
|
|
28
|
-
"build": "tsup"
|
|
37
|
+
"build": "tsup src/index.ts",
|
|
38
|
+
"start": "node dist/index.js",
|
|
39
|
+
"dev": "ts-node src/index.ts",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"lint": "eslint src/**/*.ts"
|
|
29
43
|
}
|
|
30
44
|
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
name: Publish to npm (pnpm)
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- "v*" # triggers on tags like v1.0.0
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
publish:
|
|
10
|
-
name: Publish package
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
environment:
|
|
13
|
-
name: releases
|
|
14
|
-
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v3
|
|
17
|
-
|
|
18
|
-
- name: Setup Node.js
|
|
19
|
-
uses: actions/setup-node@v4
|
|
20
|
-
with:
|
|
21
|
-
node-version: "20"
|
|
22
|
-
registry-url: "https://registry.npmjs.org/"
|
|
23
|
-
|
|
24
|
-
- name: Install pnpm
|
|
25
|
-
run: npm install -g pnpm
|
|
26
|
-
|
|
27
|
-
- name: Install dependencies
|
|
28
|
-
run: pnpm install
|
|
29
|
-
|
|
30
|
-
- name: Build
|
|
31
|
-
run: pnpm build
|
|
32
|
-
|
|
33
|
-
- name: Publish to npm
|
|
34
|
-
run: pnpm publish --no-git-checks
|
|
35
|
-
env:
|
|
36
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/src/index.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { program } from 'commander';
|
|
4
|
-
import * as parser from '@babel/parser';
|
|
5
|
-
|
|
6
|
-
import _traverse from '@babel/traverse';
|
|
7
|
-
const traverse = (_traverse as any).default ?? _traverse;
|
|
8
|
-
import type { ImportDeclaration } from '@babel/types';
|
|
9
|
-
|
|
10
|
-
// Define CLI options
|
|
11
|
-
program
|
|
12
|
-
.requiredOption('-d, --dir <directory>', 'Directory to scan')
|
|
13
|
-
.requiredOption('-l, --lib <library>', 'Library name to match')
|
|
14
|
-
.parse(process.argv);
|
|
15
|
-
|
|
16
|
-
const { dir, lib } = program.opts<{
|
|
17
|
-
dir: string;
|
|
18
|
-
lib: string;
|
|
19
|
-
}>();
|
|
20
|
-
|
|
21
|
-
// Helpers
|
|
22
|
-
function isJavaScriptFile(file: string): boolean {
|
|
23
|
-
return /\.(js|ts|jsx|tsx)$/.test(file);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getAllFiles(dirPath: string, arrayOfFiles: string[] = []): string[] {
|
|
27
|
-
const files = fs.readdirSync(dirPath);
|
|
28
|
-
for (const file of files) {
|
|
29
|
-
const fullPath = path.join(dirPath, file);
|
|
30
|
-
const stat = fs.statSync(fullPath);
|
|
31
|
-
if (stat.isDirectory()) {
|
|
32
|
-
getAllFiles(fullPath, arrayOfFiles);
|
|
33
|
-
} else if (isJavaScriptFile(fullPath)) {
|
|
34
|
-
arrayOfFiles.push(fullPath);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return arrayOfFiles;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
type ImportMatch = {
|
|
41
|
-
importedName: string;
|
|
42
|
-
localName: string;
|
|
43
|
-
file: string;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
function extractImportsFromFile(filePath: string, targetLib: string): ImportMatch[] {
|
|
47
|
-
const code = fs.readFileSync(filePath, 'utf-8');
|
|
48
|
-
let ast;
|
|
49
|
-
try {
|
|
50
|
-
ast = parser.parse(code, {
|
|
51
|
-
sourceType: 'module',
|
|
52
|
-
plugins: ['typescript', 'jsx'],
|
|
53
|
-
});
|
|
54
|
-
} catch (err) {
|
|
55
|
-
console.warn(`Skipping ${filePath}: Failed to parse`);
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const matches: ImportMatch[] = [];
|
|
60
|
-
|
|
61
|
-
traverse(ast, {
|
|
62
|
-
ImportDeclaration(path: any) {
|
|
63
|
-
const node = path.node as ImportDeclaration;
|
|
64
|
-
if (node.source.value === targetLib) {
|
|
65
|
-
for (const specifier of node.specifiers) {
|
|
66
|
-
const importedName =
|
|
67
|
-
'imported' in specifier && specifier.imported
|
|
68
|
-
? specifier.imported.name as any
|
|
69
|
-
: 'default';
|
|
70
|
-
const localName = specifier.local.name;
|
|
71
|
-
|
|
72
|
-
matches.push({
|
|
73
|
-
importedName,
|
|
74
|
-
localName,
|
|
75
|
-
file: filePath,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
return matches;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Process all files and build result
|
|
86
|
-
const allFiles = getAllFiles(dir);
|
|
87
|
-
const componentMap: Record<string, Set<string>> = {};
|
|
88
|
-
|
|
89
|
-
for (const file of allFiles) {
|
|
90
|
-
const imports = extractImportsFromFile(file, lib);
|
|
91
|
-
for (const { importedName, file: filePath } of imports) {
|
|
92
|
-
if (!componentMap[importedName]) {
|
|
93
|
-
componentMap[importedName] = new Set();
|
|
94
|
-
}
|
|
95
|
-
componentMap[importedName].add(filePath);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Final output
|
|
100
|
-
const output: Record<string, string[]> = {};
|
|
101
|
-
for (const [component, files] of Object.entries(componentMap)) {
|
|
102
|
-
output[component] = [...files];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
console.log(JSON.stringify(output, null, 2));
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "Node",
|
|
6
|
-
"esModuleInterop": true,
|
|
7
|
-
"forceConsistentCasingInFileNames": true,
|
|
8
|
-
"verbatimModuleSyntax": true,
|
|
9
|
-
"strict": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"outDir": "dist"
|
|
12
|
-
},
|
|
13
|
-
"include": ["src"]
|
|
14
|
-
}
|
package/tsup.config.ts
DELETED