importree 1.0.1 → 2.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/README.md +48 -42
- package/dist/index.cjs +146 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -5
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +39 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +146 -75
- package/dist/index.mjs.map +1 -1
- package/package.json +30 -21
package/README.md
CHANGED
|
@@ -29,28 +29,28 @@ Measured with [Vitest bench](https://vitest.dev/guide/features.html#benchmarking
|
|
|
29
29
|
|
|
30
30
|
Each tool brings different strengths — [dependency-tree](https://github.com/dependents/node-dependency-tree) offers robust AST-based analysis via detective, [madge](https://github.com/pahen/madge) supports multiple languages and provides circular dependency detection with visualization. importree trades those features for raw speed through regex-based extraction.
|
|
31
31
|
|
|
32
|
-
| Scenario
|
|
33
|
-
|
|
34
|
-
| Small (10 files)
|
|
35
|
-
| Medium (100 files) | **
|
|
36
|
-
| Large (500 files)
|
|
32
|
+
| Scenario | importree | [dependency-tree](https://github.com/dependents/node-dependency-tree) | [madge](https://github.com/pahen/madge) | Manual glob+regex | ts.createProgram |
|
|
33
|
+
| ------------------ | ---------- | --------------------------------------------------------------------- | --------------------------------------- | ----------------- | ---------------- |
|
|
34
|
+
| Small (10 files) | **0.2 ms** | 1.3 ms | 1.4 ms | 0.7 ms | ~100 ms |
|
|
35
|
+
| Medium (100 files) | **1.1 ms** | 8.7 ms | 8.8 ms | 5.6 ms | ~100 ms |
|
|
36
|
+
| Large (500 files) | **6.0 ms** | 21.5 ms | 25.8 ms | 27.6 ms | ~100 ms |
|
|
37
37
|
|
|
38
38
|
### Full tree build
|
|
39
39
|
|
|
40
|
-
| Project size | Mean time | Throughput
|
|
41
|
-
|
|
42
|
-
| 10 files
|
|
43
|
-
| 100 files
|
|
44
|
-
| 500 files
|
|
45
|
-
| 1,000 files
|
|
40
|
+
| Project size | Mean time | Throughput |
|
|
41
|
+
| ------------ | --------- | ------------ |
|
|
42
|
+
| 10 files | 0.2 ms | ~5,121 ops/s |
|
|
43
|
+
| 100 files | 1.2 ms | ~863 ops/s |
|
|
44
|
+
| 500 files | 5.1 ms | ~197 ops/s |
|
|
45
|
+
| 1,000 files | 10.3 ms | ~97 ops/s |
|
|
46
46
|
|
|
47
47
|
### Scanner throughput
|
|
48
48
|
|
|
49
|
-
| Operation
|
|
50
|
-
|
|
51
|
-
| `scanImports` (3 imports)
|
|
52
|
-
| `scanImports` (50 imports)
|
|
53
|
-
| `stripComments` (1,000 lines) | ~
|
|
49
|
+
| Operation | Throughput |
|
|
50
|
+
| ----------------------------- | ------------- |
|
|
51
|
+
| `scanImports` (3 imports) | ~706K ops/s |
|
|
52
|
+
| `scanImports` (50 imports) | ~79K ops/s |
|
|
53
|
+
| `stripComments` (1,000 lines) | ~12,337 ops/s |
|
|
54
54
|
|
|
55
55
|
> Run `pnpm bench:run` to reproduce locally.
|
|
56
56
|
|
|
@@ -58,6 +58,12 @@ Each tool brings different strengths — [dependency-tree](https://github.com/de
|
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
60
|
npm install importree
|
|
61
|
+
# or
|
|
62
|
+
pnpm add importree
|
|
63
|
+
# or
|
|
64
|
+
yarn add importree
|
|
65
|
+
# or
|
|
66
|
+
bun add importree
|
|
61
67
|
```
|
|
62
68
|
|
|
63
69
|
Requires Node.js >= 18.
|
|
@@ -67,10 +73,10 @@ Requires Node.js >= 18.
|
|
|
67
73
|
### Build the tree
|
|
68
74
|
|
|
69
75
|
```ts
|
|
70
|
-
import { importree } from
|
|
76
|
+
import { importree } from "importree";
|
|
71
77
|
|
|
72
|
-
const tree = await importree(
|
|
73
|
-
aliases: {
|
|
78
|
+
const tree = await importree("./src/index.ts", {
|
|
79
|
+
aliases: { "@": "./src" },
|
|
74
80
|
});
|
|
75
81
|
|
|
76
82
|
console.log(tree.files);
|
|
@@ -86,12 +92,12 @@ console.log(tree.graph);
|
|
|
86
92
|
### Find affected files
|
|
87
93
|
|
|
88
94
|
```ts
|
|
89
|
-
import { importree, getAffectedFiles } from
|
|
95
|
+
import { importree, getAffectedFiles } from "importree";
|
|
90
96
|
|
|
91
|
-
const tree = await importree(
|
|
97
|
+
const tree = await importree("./src/index.ts");
|
|
92
98
|
|
|
93
99
|
// When utils.ts changes, what needs rebuilding?
|
|
94
|
-
const affected = getAffectedFiles(tree,
|
|
100
|
+
const affected = getAffectedFiles(tree, "./src/utils.ts");
|
|
95
101
|
|
|
96
102
|
console.log(affected);
|
|
97
103
|
// ['/abs/src/app.ts', '/abs/src/index.ts']
|
|
@@ -110,18 +116,18 @@ importree(entry: string, options?: ImportreeOptions): Promise<ImportTree>
|
|
|
110
116
|
|
|
111
117
|
#### Parameters
|
|
112
118
|
|
|
113
|
-
| Parameter | Type
|
|
114
|
-
|
|
115
|
-
| `entry`
|
|
116
|
-
| `options` | `ImportreeOptions` | No
|
|
119
|
+
| Parameter | Type | Required | Description |
|
|
120
|
+
| --------- | ------------------ | -------- | ----------------------------------------------- |
|
|
121
|
+
| `entry` | `string` | Yes | Path to the entry file (resolved against `cwd`) |
|
|
122
|
+
| `options` | `ImportreeOptions` | No | Configuration for resolution behavior |
|
|
117
123
|
|
|
118
124
|
#### `ImportreeOptions`
|
|
119
125
|
|
|
120
|
-
| Option
|
|
121
|
-
|
|
122
|
-
| `rootDir`
|
|
123
|
-
| `aliases`
|
|
124
|
-
| `extensions` | `string[]`
|
|
126
|
+
| Option | Type | Default | Description |
|
|
127
|
+
| ------------ | ------------------------ | ------------------------------------------------ | ----------------------------------------------------------- |
|
|
128
|
+
| `rootDir` | `string` | `process.cwd()` | Root directory for resolving relative alias paths |
|
|
129
|
+
| `aliases` | `Record<string, string>` | `{}` | Path alias mappings (e.g., `{ '@': './src' }`) |
|
|
130
|
+
| `extensions` | `string[]` | `['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']` | File extensions to try when resolving extensionless imports |
|
|
125
131
|
|
|
126
132
|
#### Returns
|
|
127
133
|
|
|
@@ -139,10 +145,10 @@ getAffectedFiles(tree: ImportTree, changedFile: string): string[]
|
|
|
139
145
|
|
|
140
146
|
#### Parameters
|
|
141
147
|
|
|
142
|
-
| Parameter
|
|
143
|
-
|
|
144
|
-
| `tree`
|
|
145
|
-
| `changedFile` | `string`
|
|
148
|
+
| Parameter | Type | Required | Description |
|
|
149
|
+
| ------------- | ------------ | -------- | ---------------------------------------------------- |
|
|
150
|
+
| `tree` | `ImportTree` | Yes | A tree previously returned by `importree()` |
|
|
151
|
+
| `changedFile` | `string` | Yes | Path to the file that changed (resolved to absolute) |
|
|
146
152
|
|
|
147
153
|
#### Returns
|
|
148
154
|
|
|
@@ -154,13 +160,13 @@ getAffectedFiles(tree: ImportTree, changedFile: string): string[]
|
|
|
154
160
|
|
|
155
161
|
The result object returned by `importree()`.
|
|
156
162
|
|
|
157
|
-
| Field
|
|
158
|
-
|
|
159
|
-
| `entrypoint`
|
|
160
|
-
| `files`
|
|
161
|
-
| `externals`
|
|
162
|
-
| `graph`
|
|
163
|
-
| `reverseGraph` | `Record<string, string[]>` | Reverse adjacency list. Each file maps to files that import it.
|
|
163
|
+
| Field | Type | Description |
|
|
164
|
+
| -------------- | -------------------------- | --------------------------------------------------------------------------------- |
|
|
165
|
+
| `entrypoint` | `string` | Absolute path of the entry file |
|
|
166
|
+
| `files` | `string[]` | Sorted absolute paths of all local files in the dependency tree |
|
|
167
|
+
| `externals` | `string[]` | Sorted unique bare import specifiers — packages like `react`, `lodash`, `node:fs` |
|
|
168
|
+
| `graph` | `Record<string, string[]>` | Forward adjacency list. Each file maps to its direct local imports. |
|
|
169
|
+
| `reverseGraph` | `Record<string, string[]>` | Reverse adjacency list. Each file maps to files that import it. |
|
|
164
170
|
|
|
165
171
|
## What gets detected
|
|
166
172
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,101 +1,88 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
let node_path = require("node:path");
|
|
3
|
-
let node_fs_promises = require("node:fs/promises");
|
|
4
2
|
let node_fs = require("node:fs");
|
|
3
|
+
let node_path = require("node:path");
|
|
5
4
|
//#region src/scanner.ts
|
|
6
5
|
/**
|
|
7
6
|
* Strips comments from source code while preserving string literals.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
8
|
+
* Line comments (`//`) are removed entirely. Block comments are replaced
|
|
9
|
+
* with a single space (newlines within them are preserved). Strings and
|
|
10
10
|
* template literals are left intact so that import specifiers inside
|
|
11
11
|
* `from 'specifier'` remain extractable. The function correctly handles
|
|
12
12
|
* comment-like sequences inside strings (e.g., `'//'` won't start a comment).
|
|
13
13
|
*/
|
|
14
14
|
function stripComments(code) {
|
|
15
15
|
const len = code.length;
|
|
16
|
-
const
|
|
16
|
+
const parts = [];
|
|
17
17
|
let i = 0;
|
|
18
|
+
let segStart = 0;
|
|
18
19
|
while (i < len) {
|
|
19
20
|
const ch = code[i];
|
|
20
21
|
const next = i + 1 < len ? code[i + 1] : "";
|
|
21
22
|
if (ch === "/" && next === "/") {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
parts.push(code.slice(segStart, i));
|
|
24
|
+
while (i < len && code[i] !== "\n") i++;
|
|
25
|
+
segStart = i;
|
|
25
26
|
continue;
|
|
26
27
|
}
|
|
27
28
|
if (ch === "/" && next === "*") {
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
parts.push(code.slice(segStart, i));
|
|
30
|
+
i += 2;
|
|
30
31
|
while (i < len && !(code[i] === "*" && i + 1 < len && code[i + 1] === "/")) {
|
|
31
|
-
|
|
32
|
+
if (code[i] === "\n") parts.push("\n");
|
|
32
33
|
i++;
|
|
33
34
|
}
|
|
34
|
-
if (i < len)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
35
|
+
if (i < len) i += 2;
|
|
36
|
+
parts.push(" ");
|
|
37
|
+
segStart = i;
|
|
38
38
|
continue;
|
|
39
39
|
}
|
|
40
40
|
if (ch === "'" || ch === "\"") {
|
|
41
41
|
const quote = ch;
|
|
42
|
-
result[i] = code[i];
|
|
43
42
|
i++;
|
|
44
|
-
while (i < len && code[i] !== quote)
|
|
45
|
-
|
|
46
|
-
i++;
|
|
47
|
-
result[i] = code[i];
|
|
48
|
-
i++;
|
|
49
|
-
} else {
|
|
50
|
-
result[i] = code[i];
|
|
51
|
-
i++;
|
|
52
|
-
}
|
|
53
|
-
if (i < len) {
|
|
54
|
-
result[i] = code[i];
|
|
43
|
+
while (i < len && code[i] !== quote) {
|
|
44
|
+
if (code[i] === "\\" && i + 1 < len) i++;
|
|
55
45
|
i++;
|
|
56
46
|
}
|
|
47
|
+
if (i < len) i++;
|
|
57
48
|
continue;
|
|
58
49
|
}
|
|
59
50
|
if (ch === "`") {
|
|
60
|
-
result[i] = code[i];
|
|
61
51
|
i++;
|
|
62
52
|
let depth = 0;
|
|
63
|
-
while (i < len) if (code[i] === "\\" && i + 1 < len)
|
|
64
|
-
|
|
65
|
-
i
|
|
66
|
-
result[i] = code[i];
|
|
67
|
-
i++;
|
|
68
|
-
} else if (code[i] === "$" && i + 1 < len && code[i + 1] === "{") {
|
|
69
|
-
result[i] = code[i];
|
|
70
|
-
i++;
|
|
71
|
-
result[i] = code[i];
|
|
72
|
-
i++;
|
|
53
|
+
while (i < len) if (code[i] === "\\" && i + 1 < len) i += 2;
|
|
54
|
+
else if (code[i] === "$" && i + 1 < len && code[i + 1] === "{") {
|
|
55
|
+
i += 2;
|
|
73
56
|
depth++;
|
|
74
57
|
} else if (code[i] === "}" && depth > 0) {
|
|
75
|
-
result[i] = code[i];
|
|
76
58
|
i++;
|
|
77
59
|
depth--;
|
|
78
60
|
} else if (code[i] === "`" && depth === 0) {
|
|
79
|
-
result[i] = code[i];
|
|
80
61
|
i++;
|
|
81
62
|
break;
|
|
82
|
-
} else
|
|
83
|
-
result[i] = code[i];
|
|
84
|
-
i++;
|
|
85
|
-
}
|
|
63
|
+
} else i++;
|
|
86
64
|
continue;
|
|
87
65
|
}
|
|
88
|
-
result[i] = ch;
|
|
89
66
|
i++;
|
|
90
67
|
}
|
|
91
|
-
|
|
68
|
+
parts.push(code.slice(segStart));
|
|
69
|
+
return parts.join("");
|
|
92
70
|
}
|
|
93
|
-
const
|
|
71
|
+
const nsImportRe = /\bimport\s+\*\s+as\s+[$\w]+\s+from\s+['"]([^'"]+)['"]/g;
|
|
72
|
+
const namedImportRe = /\bimport\s+(?:type\s+)?(?:([$\w]+)\s*,\s*)?\{([^}]*)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
73
|
+
const defaultImportRe = /\bimport\s+(?:type\s+)?([$\w]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
74
|
+
const reexportStarRe = /\bexport\s+\*\s+(?:as\s+[$\w]+\s+)?from\s+['"]([^'"]+)['"]/g;
|
|
75
|
+
const reexportNamedRe = /\bexport\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
94
76
|
const sideEffectRe = /\bimport\s+['"]([^'"]+)['"]/g;
|
|
95
77
|
const dynamicRe = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
96
78
|
const requireRe = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
79
|
+
function parseSpecifiers(clause) {
|
|
80
|
+
return clause.split(",").map((s) => s.trim()).filter(Boolean).map((s) => {
|
|
81
|
+
return s.replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
|
|
82
|
+
}).filter(Boolean);
|
|
83
|
+
}
|
|
97
84
|
/**
|
|
98
|
-
* Scans source code and extracts all
|
|
85
|
+
* Scans source code and extracts all imports with metadata.
|
|
99
86
|
*
|
|
100
87
|
* Handles: static imports, dynamic imports, require(), re-exports.
|
|
101
88
|
* Ignores imports inside comments. Imports inside string literals may
|
|
@@ -104,12 +91,41 @@ const requireRe = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
|
104
91
|
*/
|
|
105
92
|
function scanImports(code) {
|
|
106
93
|
const stripped = stripComments(code);
|
|
107
|
-
const
|
|
108
|
-
for (const m of stripped.matchAll(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const m of stripped.matchAll(nsImportRe)) results.push({
|
|
96
|
+
path: m[1],
|
|
97
|
+
isNamespace: true
|
|
98
|
+
});
|
|
99
|
+
for (const m of stripped.matchAll(namedImportRe)) {
|
|
100
|
+
const specifiers = parseSpecifiers(m[2]);
|
|
101
|
+
if (m[1]) specifiers.unshift("default");
|
|
102
|
+
results.push({
|
|
103
|
+
path: m[3],
|
|
104
|
+
specifiers
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
for (const m of stripped.matchAll(defaultImportRe)) results.push({
|
|
108
|
+
path: m[2],
|
|
109
|
+
specifiers: ["default"]
|
|
110
|
+
});
|
|
111
|
+
for (const m of stripped.matchAll(reexportStarRe)) results.push({
|
|
112
|
+
path: m[1],
|
|
113
|
+
isNamespace: true
|
|
114
|
+
});
|
|
115
|
+
for (const m of stripped.matchAll(reexportNamedRe)) results.push({
|
|
116
|
+
path: m[2],
|
|
117
|
+
specifiers: parseSpecifiers(m[1])
|
|
118
|
+
});
|
|
119
|
+
for (const m of stripped.matchAll(sideEffectRe)) results.push({
|
|
120
|
+
path: m[1],
|
|
121
|
+
isSideEffect: true
|
|
122
|
+
});
|
|
123
|
+
for (const m of stripped.matchAll(dynamicRe)) results.push({
|
|
124
|
+
path: m[1],
|
|
125
|
+
isDynamic: true
|
|
126
|
+
});
|
|
127
|
+
for (const m of stripped.matchAll(requireRe)) results.push({ path: m[1] });
|
|
128
|
+
return results;
|
|
113
129
|
}
|
|
114
130
|
//#endregion
|
|
115
131
|
//#region src/resolver.ts
|
|
@@ -193,9 +209,43 @@ function createResolver(basedir, options) {
|
|
|
193
209
|
}
|
|
194
210
|
//#endregion
|
|
195
211
|
//#region src/walker.ts
|
|
212
|
+
function buildEdges(rawImports, resolveSpecifier, filePath) {
|
|
213
|
+
const edges = [];
|
|
214
|
+
const externals = [];
|
|
215
|
+
for (const raw of rawImports) {
|
|
216
|
+
const resolved = resolveSpecifier(raw.path, filePath);
|
|
217
|
+
if (!resolved) continue;
|
|
218
|
+
if (resolved.type === "external" && resolved.specifier) externals.push(resolved.specifier);
|
|
219
|
+
else if (resolved.type === "local" && resolved.absolutePath) {
|
|
220
|
+
const existing = edges.find((e) => e.path === resolved.absolutePath);
|
|
221
|
+
if (existing) {
|
|
222
|
+
if (raw.isNamespace) {
|
|
223
|
+
existing.isNamespace = true;
|
|
224
|
+
existing.specifiers = void 0;
|
|
225
|
+
existing.isSideEffect = void 0;
|
|
226
|
+
} else if (raw.specifiers && !existing.isNamespace) {
|
|
227
|
+
(existing.specifiers ??= []).push(...raw.specifiers);
|
|
228
|
+
existing.isSideEffect = void 0;
|
|
229
|
+
}
|
|
230
|
+
if (raw.isSideEffect && !existing.specifiers && !existing.isNamespace) existing.isSideEffect = true;
|
|
231
|
+
if (raw.isDynamic) existing.isDynamic = true;
|
|
232
|
+
} else edges.push({
|
|
233
|
+
path: resolved.absolutePath,
|
|
234
|
+
specifiers: raw.specifiers,
|
|
235
|
+
isNamespace: raw.isNamespace,
|
|
236
|
+
isDynamic: raw.isDynamic,
|
|
237
|
+
isSideEffect: raw.isSideEffect
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
edges,
|
|
243
|
+
externals
|
|
244
|
+
};
|
|
245
|
+
}
|
|
196
246
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
247
|
+
* Walks imports starting from an entry file and builds the full dependency tree.
|
|
248
|
+
* Uses iterative DFS with an explicit stack to avoid call-stack limits on deep chains.
|
|
199
249
|
*/
|
|
200
250
|
async function walk(entryFile, options) {
|
|
201
251
|
const entrypoint = (0, node_path.resolve)(entryFile);
|
|
@@ -203,26 +253,27 @@ async function walk(entryFile, options) {
|
|
|
203
253
|
const graph = {};
|
|
204
254
|
const externals = /* @__PURE__ */ new Set();
|
|
205
255
|
const visited = /* @__PURE__ */ new Set();
|
|
206
|
-
|
|
207
|
-
|
|
256
|
+
const stack = [entrypoint];
|
|
257
|
+
while (stack.length > 0) {
|
|
258
|
+
const filePath = stack.pop();
|
|
259
|
+
if (visited.has(filePath)) continue;
|
|
208
260
|
visited.add(filePath);
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (!resolved) continue;
|
|
214
|
-
if (resolved.type === "external" && resolved.specifier) externals.add(resolved.specifier);
|
|
215
|
-
else if (resolved.type === "local" && resolved.absolutePath) localDeps.push(resolved.absolutePath);
|
|
216
|
-
}
|
|
217
|
-
graph[filePath] = localDeps;
|
|
218
|
-
await Promise.all(localDeps.map((dep) => visit(dep)));
|
|
261
|
+
const { edges, externals: fileExternals } = buildEdges(scanImports((0, node_fs.readFileSync)(filePath, "utf-8")), resolveSpecifier, filePath);
|
|
262
|
+
for (const ext of fileExternals) externals.add(ext);
|
|
263
|
+
graph[filePath] = edges;
|
|
264
|
+
for (const edge of edges) stack.push(edge.path);
|
|
219
265
|
}
|
|
220
|
-
await visit(entrypoint);
|
|
221
266
|
const reverseGraph = {};
|
|
222
267
|
for (const file of Object.keys(graph)) reverseGraph[file] = [];
|
|
223
|
-
for (const [file,
|
|
224
|
-
if (!reverseGraph[
|
|
225
|
-
reverseGraph[
|
|
268
|
+
for (const [file, edges] of Object.entries(graph)) for (const edge of edges) {
|
|
269
|
+
if (!reverseGraph[edge.path]) reverseGraph[edge.path] = [];
|
|
270
|
+
reverseGraph[edge.path].push({
|
|
271
|
+
path: file,
|
|
272
|
+
specifiers: edge.specifiers,
|
|
273
|
+
isNamespace: edge.isNamespace,
|
|
274
|
+
isDynamic: edge.isDynamic,
|
|
275
|
+
isSideEffect: edge.isSideEffect
|
|
276
|
+
});
|
|
226
277
|
}
|
|
227
278
|
return {
|
|
228
279
|
entrypoint,
|
|
@@ -255,6 +306,26 @@ async function importree(entry, options) {
|
|
|
255
306
|
return walk(entry, options ?? {});
|
|
256
307
|
}
|
|
257
308
|
/**
|
|
309
|
+
* Parses a single file and returns its direct import edges without recursive traversal.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* const edges = parseImports('./src/components/Button.tsx', {
|
|
314
|
+
* aliases: { '@': './src' },
|
|
315
|
+
* });
|
|
316
|
+
*
|
|
317
|
+
* for (const edge of edges) {
|
|
318
|
+
* console.log(edge.path, edge.specifiers);
|
|
319
|
+
* }
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
function parseImports(filePath, options) {
|
|
323
|
+
const absolutePath = (0, node_path.resolve)(filePath);
|
|
324
|
+
const resolveSpecifier = createResolver(options?.rootDir ? (0, node_path.resolve)(options.rootDir) : process.cwd(), options ?? {});
|
|
325
|
+
const { edges } = buildEdges(scanImports((0, node_fs.readFileSync)(absolutePath, "utf-8")), resolveSpecifier, absolutePath);
|
|
326
|
+
return edges;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
258
329
|
* Given an import tree and a changed file, returns all files that
|
|
259
330
|
* transitively depend on the changed file (i.e., files that would
|
|
260
331
|
* need to be re-evaluated if the changed file is modified).
|
|
@@ -270,9 +341,9 @@ function getAffectedFiles(tree, changedFile) {
|
|
|
270
341
|
const current = queue.shift();
|
|
271
342
|
const dependents = tree.reverseGraph[current];
|
|
272
343
|
if (!dependents) continue;
|
|
273
|
-
for (const
|
|
274
|
-
affected.add(
|
|
275
|
-
queue.push(
|
|
344
|
+
for (const edge of dependents) if (!affected.has(edge.path)) {
|
|
345
|
+
affected.add(edge.path);
|
|
346
|
+
queue.push(edge.path);
|
|
276
347
|
}
|
|
277
348
|
}
|
|
278
349
|
affected.delete(absolute);
|
|
@@ -281,5 +352,6 @@ function getAffectedFiles(tree, changedFile) {
|
|
|
281
352
|
//#endregion
|
|
282
353
|
exports.getAffectedFiles = getAffectedFiles;
|
|
283
354
|
exports.importree = importree;
|
|
355
|
+
exports.parseImports = parseImports;
|
|
284
356
|
|
|
285
357
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/scanner.ts","../src/resolver.ts","../src/walker.ts","../src/index.ts"],"sourcesContent":["/**\n * Strips comments from source code while preserving string literals.\n *\n * Comments are replaced with spaces (preserving newlines). Strings and\n * template literals are left intact so that import specifiers inside\n * `from 'specifier'` remain extractable. The function correctly handles\n * comment-like sequences inside strings (e.g., `'//'` won't start a comment).\n */\nexport function stripComments(code: string): string {\n const len = code.length;\n const result: string[] = new Array(len);\n let i = 0;\n\n while (i < len) {\n const ch = code[i];\n const next = i + 1 < len ? code[i + 1] : '';\n\n // Line comment → blank to end of line\n if (ch === '/' && next === '/') {\n result[i++] = ' ';\n result[i++] = ' ';\n while (i < len && code[i] !== '\\n') {\n result[i++] = ' ';\n }\n continue;\n }\n\n // Block comment → blank to closing */\n if (ch === '/' && next === '*') {\n result[i++] = ' ';\n result[i++] = ' ';\n while (i < len && !(code[i] === '*' && i + 1 < len && code[i + 1] === '/')) {\n result[i] = code[i] === '\\n' ? '\\n' : ' ';\n i++;\n }\n if (i < len) {\n result[i++] = ' '; // *\n result[i++] = ' '; // /\n }\n continue;\n }\n\n // Single or double quoted string — copy verbatim (skip past to avoid\n // misidentifying comment markers inside strings)\n if (ch === \"'\" || ch === '\"') {\n const quote = ch;\n result[i] = code[i];\n i++;\n while (i < len && code[i] !== quote) {\n if (code[i] === '\\\\' && i + 1 < len) {\n result[i] = code[i];\n i++;\n result[i] = code[i];\n i++;\n } else {\n result[i] = code[i];\n i++;\n }\n }\n if (i < len) {\n result[i] = code[i];\n i++;\n }\n continue;\n }\n\n // Template literal — copy verbatim, handling ${} nesting\n if (ch === '`') {\n result[i] = code[i];\n i++;\n let depth = 0;\n while (i < len) {\n if (code[i] === '\\\\' && i + 1 < len) {\n result[i] = code[i];\n i++;\n result[i] = code[i];\n i++;\n } else if (code[i] === '$' && i + 1 < len && code[i + 1] === '{') {\n result[i] = code[i];\n i++;\n result[i] = code[i];\n i++;\n depth++;\n } else if (code[i] === '}' && depth > 0) {\n result[i] = code[i];\n i++;\n depth--;\n } else if (code[i] === '`' && depth === 0) {\n result[i] = code[i];\n i++;\n break;\n } else {\n result[i] = code[i];\n i++;\n }\n }\n continue;\n }\n\n // Regular character\n result[i] = ch;\n i++;\n }\n\n return result.join('');\n}\n\n// Static regex patterns — compiled once\nconst fromRe = /\\bfrom\\s+['\"]([^'\"]+)['\"]/g;\nconst sideEffectRe = /\\bimport\\s+['\"]([^'\"]+)['\"]/g;\nconst dynamicRe = /\\bimport\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\nconst requireRe = /\\brequire\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n\n/**\n * Scans source code and extracts all import/require specifiers.\n *\n * Handles: static imports, dynamic imports, require(), re-exports.\n * Ignores imports inside comments. Imports inside string literals may\n * produce false positives, but unresolvable paths are silently skipped\n * by the resolver.\n */\nexport function scanImports(code: string): string[] {\n const stripped = stripComments(code);\n const specifiers = new Set<string>();\n\n for (const m of stripped.matchAll(fromRe)) specifiers.add(m[1]);\n for (const m of stripped.matchAll(sideEffectRe)) specifiers.add(m[1]);\n for (const m of stripped.matchAll(dynamicRe)) specifiers.add(m[1]);\n for (const m of stripped.matchAll(requireRe)) specifiers.add(m[1]);\n\n return [...specifiers];\n}\n","import { statSync } from 'node:fs';\nimport { dirname, join, resolve, isAbsolute } from 'node:path';\nimport type { ImportreeOptions, ResolvedImport } from './types.js';\n\nconst DEFAULT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];\n\nfunction fileExists(filePath: string): boolean {\n const stat = statSync(filePath, { throwIfNoEntry: false });\n return stat !== undefined && stat.isFile();\n}\n\nfunction dirExists(filePath: string): boolean {\n const stat = statSync(filePath, { throwIfNoEntry: false });\n return stat !== undefined && stat.isDirectory();\n}\n\n/**\n * Extract the bare package name from an import specifier.\n * - Scoped: `@scope/pkg/path` → `@scope/pkg`\n * - Unscoped: `pkg/path` → `pkg`\n */\nfunction getBareSpecifier(specifier: string): string {\n if (specifier.startsWith('@')) {\n const parts = specifier.split('/');\n return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;\n }\n return specifier.split('/')[0];\n}\n\nfunction resolveFile(\n filePath: string,\n extensions: string[],\n): string | undefined {\n // Try exact path\n if (fileExists(filePath)) return filePath;\n\n // Try with each extension\n for (const ext of extensions) {\n const withExt = filePath + ext;\n if (fileExists(withExt)) return withExt;\n }\n\n // Try as directory with index file\n if (dirExists(filePath)) {\n for (const ext of extensions) {\n const indexPath = join(filePath, `index${ext}`);\n if (fileExists(indexPath)) return indexPath;\n }\n }\n\n return undefined;\n}\n\nexport interface Resolver {\n (specifier: string, fromFile: string): ResolvedImport | undefined;\n}\n\n/**\n * Creates a resolver function that resolves import specifiers to absolute\n * file paths, with support for aliases and extension probing.\n */\nexport function createResolver(\n basedir: string,\n options: ImportreeOptions,\n): Resolver {\n const extensions = options.extensions ?? DEFAULT_EXTENSIONS;\n\n // Sort aliases by key length descending for longest-prefix matching\n const aliases = options.aliases\n ? Object.entries(options.aliases).sort((a, b) => b[0].length - a[0].length)\n : [];\n\n const resolvedAliasValues = aliases.map(([key, value]) => [\n key,\n isAbsolute(value) ? value : resolve(basedir, value),\n ] as const);\n\n const cache = new Map<string, ResolvedImport | undefined>();\n\n return function resolveSpecifier(\n specifier: string,\n fromFile: string,\n ): ResolvedImport | undefined {\n const fromDir = dirname(fromFile);\n const cacheKey = `${specifier}\\0${fromDir}`;\n\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n\n let result: ResolvedImport | undefined;\n\n // Relative import\n if (specifier.startsWith('./') || specifier.startsWith('../')) {\n const absolutePath = resolveFile(resolve(fromDir, specifier), extensions);\n if (absolutePath) {\n result = { type: 'local', absolutePath };\n }\n }\n // Check aliases\n else {\n let matched = false;\n for (const [prefix, replacement] of resolvedAliasValues) {\n if (specifier === prefix || specifier.startsWith(prefix + '/')) {\n const rest = specifier === prefix ? '' : specifier.slice(prefix.length);\n const absolutePath = resolveFile(join(replacement, rest), extensions);\n if (absolutePath) {\n result = { type: 'local', absolutePath };\n }\n matched = true;\n break;\n }\n }\n\n // Bare specifier → external\n if (!matched) {\n result = { type: 'external', specifier: getBareSpecifier(specifier) };\n }\n }\n\n cache.set(cacheKey, result);\n return result;\n };\n}\n","import { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport type { ImportreeOptions, ImportTree } from './types.js';\nimport { scanImports } from './scanner.js';\nimport { createResolver } from './resolver.js';\n\n/**\n * Recursively walks imports starting from an entry file and builds\n * the full dependency tree.\n */\nexport async function walk(\n entryFile: string,\n options: ImportreeOptions,\n): Promise<ImportTree> {\n const entrypoint = resolve(entryFile);\n const basedir = options.rootDir ? resolve(options.rootDir) : process.cwd();\n const resolveSpecifier = createResolver(basedir, options);\n\n const graph: Record<string, string[]> = {};\n const externals = new Set<string>();\n const visited = new Set<string>();\n\n async function visit(filePath: string): Promise<void> {\n if (visited.has(filePath)) return;\n visited.add(filePath);\n\n const content = await readFile(filePath, 'utf-8');\n const specifiers = scanImports(content);\n\n const localDeps: string[] = [];\n for (const spec of specifiers) {\n const resolved = resolveSpecifier(spec, filePath);\n if (!resolved) continue;\n\n if (resolved.type === 'external' && resolved.specifier) {\n externals.add(resolved.specifier);\n } else if (resolved.type === 'local' && resolved.absolutePath) {\n localDeps.push(resolved.absolutePath);\n }\n }\n\n graph[filePath] = localDeps;\n\n await Promise.all(localDeps.map((dep) => visit(dep)));\n }\n\n await visit(entrypoint);\n\n // Build reverse graph\n const reverseGraph: Record<string, string[]> = {};\n for (const file of Object.keys(graph)) {\n reverseGraph[file] = [];\n }\n for (const [file, deps] of Object.entries(graph)) {\n for (const dep of deps) {\n if (!reverseGraph[dep]) reverseGraph[dep] = [];\n reverseGraph[dep].push(file);\n }\n }\n\n return {\n entrypoint,\n files: Object.keys(graph).sort(),\n externals: [...externals].sort(),\n graph,\n reverseGraph,\n };\n}\n","import { resolve } from 'node:path';\nimport type { ImportreeOptions, ImportTree } from './types.js';\nimport { walk } from './walker.js';\n\nexport type { ImportreeOptions, ImportTree } from './types.js';\n\n/**\n * Builds a full import dependency tree starting from an entry file.\n *\n * Recursively resolves all static imports, dynamic imports, require() calls,\n * and re-exports. Supports path aliases for custom resolution.\n *\n * @example\n * ```ts\n * const tree = await importree('./src/index.ts', {\n * aliases: { '@': './src' },\n * });\n *\n * console.log(tree.files); // all local dependency file paths\n * console.log(tree.externals); // external package names\n * console.log(tree.graph); // file → direct dependencies\n * ```\n */\nexport async function importree(\n entry: string,\n options?: ImportreeOptions,\n): Promise<ImportTree> {\n return walk(entry, options ?? {});\n}\n\n/**\n * Given an import tree and a changed file, returns all files that\n * transitively depend on the changed file (i.e., files that would\n * need to be re-evaluated if the changed file is modified).\n *\n * The changed file itself is NOT included in the result.\n */\nexport function getAffectedFiles(\n tree: ImportTree,\n changedFile: string,\n): string[] {\n const absolute = resolve(changedFile);\n\n if (!tree.reverseGraph[absolute]) return [];\n\n const affected = new Set<string>();\n const queue = [absolute];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const dependents = tree.reverseGraph[current];\n if (!dependents) continue;\n\n for (const parent of dependents) {\n if (!affected.has(parent)) {\n affected.add(parent);\n queue.push(parent);\n }\n }\n }\n\n affected.delete(absolute);\n return [...affected].sort();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/scanner.ts","../src/resolver.ts","../src/walker.ts","../src/index.ts"],"sourcesContent":["/**\n * Strips comments from source code while preserving string literals.\n *\n * Line comments (`//`) are removed entirely. Block comments are replaced\n * with a single space (newlines within them are preserved). Strings and\n * template literals are left intact so that import specifiers inside\n * `from 'specifier'` remain extractable. The function correctly handles\n * comment-like sequences inside strings (e.g., `'//'` won't start a comment).\n */\nexport function stripComments(code: string): string {\n const len = code.length;\n const parts: string[] = [];\n let i = 0;\n let segStart = 0;\n\n while (i < len) {\n const ch = code[i];\n const next = i + 1 < len ? code[i + 1] : \"\";\n\n if (ch === \"/\" && next === \"/\") {\n parts.push(code.slice(segStart, i));\n while (i < len && code[i] !== \"\\n\") i++;\n segStart = i;\n continue;\n }\n\n if (ch === \"/\" && next === \"*\") {\n parts.push(code.slice(segStart, i));\n i += 2;\n while (i < len && !(code[i] === \"*\" && i + 1 < len && code[i + 1] === \"/\")) {\n if (code[i] === \"\\n\") parts.push(\"\\n\");\n i++;\n }\n if (i < len) i += 2;\n parts.push(\" \");\n segStart = i;\n continue;\n }\n\n if (ch === \"'\" || ch === '\"') {\n const quote = ch;\n i++;\n while (i < len && code[i] !== quote) {\n if (code[i] === \"\\\\\" && i + 1 < len) i++;\n i++;\n }\n if (i < len) i++;\n continue;\n }\n\n if (ch === \"`\") {\n i++;\n let depth = 0;\n while (i < len) {\n if (code[i] === \"\\\\\" && i + 1 < len) {\n i += 2;\n } else if (code[i] === \"$\" && i + 1 < len && code[i + 1] === \"{\") {\n i += 2;\n depth++;\n } else if (code[i] === \"}\" && depth > 0) {\n i++;\n depth--;\n } else if (code[i] === \"`\" && depth === 0) {\n i++;\n break;\n } else {\n i++;\n }\n }\n continue;\n }\n\n i++;\n }\n\n parts.push(code.slice(segStart));\n return parts.join(\"\");\n}\n\nexport interface RawImport {\n path: string;\n specifiers?: string[];\n isNamespace?: boolean;\n isDynamic?: boolean;\n isSideEffect?: boolean;\n}\n\n// Static regex patterns — compiled once\nconst nsImportRe = /\\bimport\\s+\\*\\s+as\\s+[$\\w]+\\s+from\\s+['\"]([^'\"]+)['\"]/g;\nconst namedImportRe =\n /\\bimport\\s+(?:type\\s+)?(?:([$\\w]+)\\s*,\\s*)?\\{([^}]*)\\}\\s+from\\s+['\"]([^'\"]+)['\"]/g;\nconst defaultImportRe = /\\bimport\\s+(?:type\\s+)?([$\\w]+)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\nconst reexportStarRe = /\\bexport\\s+\\*\\s+(?:as\\s+[$\\w]+\\s+)?from\\s+['\"]([^'\"]+)['\"]/g;\nconst reexportNamedRe = /\\bexport\\s+(?:type\\s+)?\\{([^}]*)\\}\\s+from\\s+['\"]([^'\"]+)['\"]/g;\nconst sideEffectRe = /\\bimport\\s+['\"]([^'\"]+)['\"]/g;\nconst dynamicRe = /\\bimport\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\nconst requireRe = /\\brequire\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n\nexport function parseSpecifiers(clause: string): string[] {\n return clause\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean)\n .map((s) => {\n const withoutType = s.replace(/^type\\s+/, \"\");\n const parts = withoutType.split(/\\s+as\\s+/);\n return parts[0].trim();\n })\n .filter(Boolean);\n}\n\n/**\n * Scans source code and extracts all imports with metadata.\n *\n * Handles: static imports, dynamic imports, require(), re-exports.\n * Ignores imports inside comments. Imports inside string literals may\n * produce false positives, but unresolvable paths are silently skipped\n * by the resolver.\n */\nexport function scanImports(code: string): RawImport[] {\n const stripped = stripComments(code);\n const results: RawImport[] = [];\n\n for (const m of stripped.matchAll(nsImportRe)) {\n results.push({ path: m[1], isNamespace: true });\n }\n\n for (const m of stripped.matchAll(namedImportRe)) {\n const specifiers = parseSpecifiers(m[2]);\n if (m[1]) specifiers.unshift(\"default\");\n results.push({ path: m[3], specifiers });\n }\n\n for (const m of stripped.matchAll(defaultImportRe)) {\n results.push({ path: m[2], specifiers: [\"default\"] });\n }\n\n for (const m of stripped.matchAll(reexportStarRe)) {\n results.push({ path: m[1], isNamespace: true });\n }\n\n for (const m of stripped.matchAll(reexportNamedRe)) {\n results.push({ path: m[2], specifiers: parseSpecifiers(m[1]) });\n }\n\n for (const m of stripped.matchAll(sideEffectRe)) {\n results.push({ path: m[1], isSideEffect: true });\n }\n\n for (const m of stripped.matchAll(dynamicRe)) {\n results.push({ path: m[1], isDynamic: true });\n }\n\n for (const m of stripped.matchAll(requireRe)) {\n results.push({ path: m[1] });\n }\n\n return results;\n}\n","import { statSync } from \"node:fs\";\nimport { dirname, join, resolve, isAbsolute } from \"node:path\";\nimport type { ImportreeOptions, ResolvedImport } from \"./types.js\";\n\nconst DEFAULT_EXTENSIONS = [\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\", \".cjs\"];\n\nfunction fileExists(filePath: string): boolean {\n const stat = statSync(filePath, { throwIfNoEntry: false });\n return stat !== undefined && stat.isFile();\n}\n\nfunction dirExists(filePath: string): boolean {\n const stat = statSync(filePath, { throwIfNoEntry: false });\n return stat !== undefined && stat.isDirectory();\n}\n\n/**\n * Extract the bare package name from an import specifier.\n * - Scoped: `@scope/pkg/path` → `@scope/pkg`\n * - Unscoped: `pkg/path` → `pkg`\n */\nfunction getBareSpecifier(specifier: string): string {\n if (specifier.startsWith(\"@\")) {\n const parts = specifier.split(\"/\");\n return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;\n }\n return specifier.split(\"/\")[0];\n}\n\nfunction resolveFile(filePath: string, extensions: string[]): string | undefined {\n // Try exact path\n if (fileExists(filePath)) return filePath;\n\n // Try with each extension\n for (const ext of extensions) {\n const withExt = filePath + ext;\n if (fileExists(withExt)) return withExt;\n }\n\n // Try as directory with index file\n if (dirExists(filePath)) {\n for (const ext of extensions) {\n const indexPath = join(filePath, `index${ext}`);\n if (fileExists(indexPath)) return indexPath;\n }\n }\n\n return undefined;\n}\n\nexport interface Resolver {\n (specifier: string, fromFile: string): ResolvedImport | undefined;\n}\n\n/**\n * Creates a resolver function that resolves import specifiers to absolute\n * file paths, with support for aliases and extension probing.\n */\nexport function createResolver(basedir: string, options: ImportreeOptions): Resolver {\n const extensions = options.extensions ?? DEFAULT_EXTENSIONS;\n\n // Sort aliases by key length descending for longest-prefix matching\n const aliases = options.aliases\n ? Object.entries(options.aliases).sort((a, b) => b[0].length - a[0].length)\n : [];\n\n const resolvedAliasValues = aliases.map(\n ([key, value]) => [key, isAbsolute(value) ? value : resolve(basedir, value)] as const,\n );\n\n const cache = new Map<string, ResolvedImport | undefined>();\n\n return function resolveSpecifier(\n specifier: string,\n fromFile: string,\n ): ResolvedImport | undefined {\n const fromDir = dirname(fromFile);\n const cacheKey = `${specifier}\\0${fromDir}`;\n\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n\n let result: ResolvedImport | undefined;\n\n // Relative import\n if (specifier.startsWith(\"./\") || specifier.startsWith(\"../\")) {\n const absolutePath = resolveFile(resolve(fromDir, specifier), extensions);\n if (absolutePath) {\n result = { type: \"local\", absolutePath };\n }\n }\n // Check aliases\n else {\n let matched = false;\n for (const [prefix, replacement] of resolvedAliasValues) {\n if (specifier === prefix || specifier.startsWith(prefix + \"/\")) {\n const rest = specifier === prefix ? \"\" : specifier.slice(prefix.length);\n const absolutePath = resolveFile(join(replacement, rest), extensions);\n if (absolutePath) {\n result = { type: \"local\", absolutePath };\n }\n matched = true;\n break;\n }\n }\n\n // Bare specifier → external\n if (!matched) {\n result = { type: \"external\", specifier: getBareSpecifier(specifier) };\n }\n }\n\n cache.set(cacheKey, result);\n return result;\n };\n}\n","import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { ImportreeOptions, ImportTree, ImportEdge } from \"./types.js\";\nimport type { RawImport } from \"./scanner.js\";\nimport { scanImports } from \"./scanner.js\";\nimport type { Resolver } from \"./resolver.js\";\nimport { createResolver } from \"./resolver.js\";\n\nexport function buildEdges(\n rawImports: RawImport[],\n resolveSpecifier: Resolver,\n filePath: string,\n): { edges: ImportEdge[]; externals: string[] } {\n const edges: ImportEdge[] = [];\n const externals: string[] = [];\n\n for (const raw of rawImports) {\n const resolved = resolveSpecifier(raw.path, filePath);\n if (!resolved) continue;\n\n if (resolved.type === \"external\" && resolved.specifier) {\n externals.push(resolved.specifier);\n } else if (resolved.type === \"local\" && resolved.absolutePath) {\n const existing = edges.find((e) => e.path === resolved.absolutePath);\n if (existing) {\n if (raw.isNamespace) {\n existing.isNamespace = true;\n existing.specifiers = undefined;\n existing.isSideEffect = undefined;\n } else if (raw.specifiers && !existing.isNamespace) {\n (existing.specifiers ??= []).push(...raw.specifiers);\n existing.isSideEffect = undefined;\n }\n if (raw.isSideEffect && !existing.specifiers && !existing.isNamespace) {\n existing.isSideEffect = true;\n }\n if (raw.isDynamic) existing.isDynamic = true;\n } else {\n edges.push({\n path: resolved.absolutePath,\n specifiers: raw.specifiers,\n isNamespace: raw.isNamespace,\n isDynamic: raw.isDynamic,\n isSideEffect: raw.isSideEffect,\n });\n }\n }\n }\n\n return { edges, externals };\n}\n\n/**\n * Walks imports starting from an entry file and builds the full dependency tree.\n * Uses iterative DFS with an explicit stack to avoid call-stack limits on deep chains.\n */\nexport async function walk(entryFile: string, options: ImportreeOptions): Promise<ImportTree> {\n const entrypoint = resolve(entryFile);\n const basedir = options.rootDir ? resolve(options.rootDir) : process.cwd();\n const resolveSpecifier = createResolver(basedir, options);\n\n const graph: Record<string, ImportEdge[]> = {};\n const externals = new Set<string>();\n const visited = new Set<string>();\n const stack = [entrypoint];\n\n while (stack.length > 0) {\n const filePath = stack.pop()!;\n if (visited.has(filePath)) continue;\n visited.add(filePath);\n\n const content = readFileSync(filePath, \"utf-8\");\n const rawImports = scanImports(content);\n const { edges, externals: fileExternals } = buildEdges(rawImports, resolveSpecifier, filePath);\n\n for (const ext of fileExternals) externals.add(ext);\n graph[filePath] = edges;\n\n for (const edge of edges) stack.push(edge.path);\n }\n\n // Build reverse graph\n const reverseGraph: Record<string, ImportEdge[]> = {};\n for (const file of Object.keys(graph)) {\n reverseGraph[file] = [];\n }\n for (const [file, edges] of Object.entries(graph)) {\n for (const edge of edges) {\n if (!reverseGraph[edge.path]) reverseGraph[edge.path] = [];\n reverseGraph[edge.path].push({\n path: file,\n specifiers: edge.specifiers,\n isNamespace: edge.isNamespace,\n isDynamic: edge.isDynamic,\n isSideEffect: edge.isSideEffect,\n });\n }\n }\n\n return {\n entrypoint,\n files: Object.keys(graph).sort(),\n externals: [...externals].sort(),\n graph,\n reverseGraph,\n };\n}\n","import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { ImportreeOptions, ImportTree, ImportEdge } from \"./types.js\";\nimport { buildEdges, walk } from \"./walker.js\";\nimport { scanImports } from \"./scanner.js\";\nimport { createResolver } from \"./resolver.js\";\n\nexport type { ImportreeOptions, ImportTree, ImportEdge } from \"./types.js\";\n\n/**\n * Builds a full import dependency tree starting from an entry file.\n *\n * Recursively resolves all static imports, dynamic imports, require() calls,\n * and re-exports. Supports path aliases for custom resolution.\n *\n * @example\n * ```ts\n * const tree = await importree('./src/index.ts', {\n * aliases: { '@': './src' },\n * });\n *\n * console.log(tree.files); // all local dependency file paths\n * console.log(tree.externals); // external package names\n * console.log(tree.graph); // file → direct dependencies\n * ```\n */\nexport async function importree(entry: string, options?: ImportreeOptions): Promise<ImportTree> {\n return walk(entry, options ?? {});\n}\n\n/**\n * Parses a single file and returns its direct import edges without recursive traversal.\n *\n * @example\n * ```ts\n * const edges = parseImports('./src/components/Button.tsx', {\n * aliases: { '@': './src' },\n * });\n *\n * for (const edge of edges) {\n * console.log(edge.path, edge.specifiers);\n * }\n * ```\n */\nexport function parseImports(filePath: string, options?: ImportreeOptions): ImportEdge[] {\n const absolutePath = resolve(filePath);\n const basedir = options?.rootDir ? resolve(options.rootDir) : process.cwd();\n const resolveSpecifier = createResolver(basedir, options ?? {});\n\n const content = readFileSync(absolutePath, \"utf-8\");\n const rawImports = scanImports(content);\n const { edges } = buildEdges(rawImports, resolveSpecifier, absolutePath);\n\n return edges;\n}\n\n/**\n * Given an import tree and a changed file, returns all files that\n * transitively depend on the changed file (i.e., files that would\n * need to be re-evaluated if the changed file is modified).\n *\n * The changed file itself is NOT included in the result.\n */\nexport function getAffectedFiles(tree: ImportTree, changedFile: string): string[] {\n const absolute = resolve(changedFile);\n\n if (!tree.reverseGraph[absolute]) return [];\n\n const affected = new Set<string>();\n const queue = [absolute];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const dependents = tree.reverseGraph[current];\n if (!dependents) continue;\n\n for (const edge of dependents) {\n if (!affected.has(edge.path)) {\n affected.add(edge.path);\n queue.push(edge.path);\n }\n }\n }\n\n affected.delete(absolute);\n return [...affected].sort();\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -23,6 +23,25 @@ interface ImportreeOptions {
|
|
|
23
23
|
extensions?: string[];
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
+
* A single directed edge in the import graph, carrying metadata about
|
|
27
|
+
* how one file imports another.
|
|
28
|
+
*/
|
|
29
|
+
interface ImportEdge {
|
|
30
|
+
/** Absolute path of the imported file. */
|
|
31
|
+
path: string;
|
|
32
|
+
/**
|
|
33
|
+
* Specifiers imported (original names, not aliases).
|
|
34
|
+
* Contains `"default"` for default imports (e.g., `import X from '...'`).
|
|
35
|
+
*/
|
|
36
|
+
specifiers?: string[];
|
|
37
|
+
/** True when the import is `import * as X from '...'` or `export * from '...'`. */
|
|
38
|
+
isNamespace?: boolean;
|
|
39
|
+
/** True when the import is `import('...')`. */
|
|
40
|
+
isDynamic?: boolean;
|
|
41
|
+
/** True when the import is `import '...'` (side-effect only). */
|
|
42
|
+
isSideEffect?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
26
45
|
* The result of building an import dependency tree.
|
|
27
46
|
*/
|
|
28
47
|
interface ImportTree {
|
|
@@ -34,14 +53,14 @@ interface ImportTree {
|
|
|
34
53
|
externals: string[];
|
|
35
54
|
/**
|
|
36
55
|
* Forward adjacency list: each key is an absolute file path, and its value
|
|
37
|
-
* is an array of
|
|
56
|
+
* is an array of import edges describing how it depends on other files.
|
|
38
57
|
*/
|
|
39
|
-
graph: Record<string,
|
|
58
|
+
graph: Record<string, ImportEdge[]>;
|
|
40
59
|
/**
|
|
41
60
|
* Reverse adjacency list: each key is an absolute file path, and its value
|
|
42
|
-
* is an array of
|
|
61
|
+
* is an array of import edges describing files that import it.
|
|
43
62
|
*/
|
|
44
|
-
reverseGraph: Record<string,
|
|
63
|
+
reverseGraph: Record<string, ImportEdge[]>;
|
|
45
64
|
}
|
|
46
65
|
//#endregion
|
|
47
66
|
//#region src/index.d.ts
|
|
@@ -64,6 +83,21 @@ interface ImportTree {
|
|
|
64
83
|
*/
|
|
65
84
|
declare function importree(entry: string, options?: ImportreeOptions): Promise<ImportTree>;
|
|
66
85
|
/**
|
|
86
|
+
* Parses a single file and returns its direct import edges without recursive traversal.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const edges = parseImports('./src/components/Button.tsx', {
|
|
91
|
+
* aliases: { '@': './src' },
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
94
|
+
* for (const edge of edges) {
|
|
95
|
+
* console.log(edge.path, edge.specifiers);
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
declare function parseImports(filePath: string, options?: ImportreeOptions): ImportEdge[];
|
|
100
|
+
/**
|
|
67
101
|
* Given an import tree and a changed file, returns all files that
|
|
68
102
|
* transitively depend on the changed file (i.e., files that would
|
|
69
103
|
* need to be re-evaluated if the changed file is modified).
|
|
@@ -72,5 +106,5 @@ declare function importree(entry: string, options?: ImportreeOptions): Promise<I
|
|
|
72
106
|
*/
|
|
73
107
|
declare function getAffectedFiles(tree: ImportTree, changedFile: string): string[];
|
|
74
108
|
//#endregion
|
|
75
|
-
export { type ImportTree, type ImportreeOptions, getAffectedFiles, importree };
|
|
109
|
+
export { type ImportEdge, type ImportTree, type ImportreeOptions, getAffectedFiles, importree, parseImports };
|
|
76
110
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";;AAGA;;UAAiB,gBAAA;EAaL;;;;EARV,OAAA;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";;AAGA;;UAAiB,gBAAA;EAaL;;;;EARV,OAAA;;;AAsBF;;;;EAdE,OAAA,GAAU,MAAA;;;;;;EAOV,UAAA;AAAA;;;;;UAOe,UAAA;;EAEf,IAAA;EAyCc;;;;EAnCd,UAAA;;EAGA,WAAA;;EAGA,SAAA;;EAGA,YAAA;AAAA;;;;UAMe,UAAA;EC3BK;ED6BpB,UAAA;;EAGA,KAAA;;EAGA,SAAA;ECnC0E;;;;EDyC1E,KAAA,EAAO,MAAA,SAAe,UAAA;;;;ACvBxB;ED6BE,YAAA,EAAc,MAAA,SAAe,UAAA;AAAA;;;;;;;;;;;;AA3C/B;;;;;;;;iBCJsB,SAAA,CAAU,KAAA,UAAe,OAAA,GAAU,gBAAA,GAAmB,OAAA,CAAQ,UAAA;;;AD2BpF;;;;;;;;;;;;iBCTgB,YAAA,CAAa,QAAA,UAAkB,OAAA,GAAU,gBAAA,GAAmB,UAAA;;;;;;;;iBAmB5D,gBAAA,CAAiB,IAAA,EAAM,UAAA,EAAY,WAAA"}
|
package/dist/index.d.mts
CHANGED
|
@@ -23,6 +23,25 @@ interface ImportreeOptions {
|
|
|
23
23
|
extensions?: string[];
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
+
* A single directed edge in the import graph, carrying metadata about
|
|
27
|
+
* how one file imports another.
|
|
28
|
+
*/
|
|
29
|
+
interface ImportEdge {
|
|
30
|
+
/** Absolute path of the imported file. */
|
|
31
|
+
path: string;
|
|
32
|
+
/**
|
|
33
|
+
* Specifiers imported (original names, not aliases).
|
|
34
|
+
* Contains `"default"` for default imports (e.g., `import X from '...'`).
|
|
35
|
+
*/
|
|
36
|
+
specifiers?: string[];
|
|
37
|
+
/** True when the import is `import * as X from '...'` or `export * from '...'`. */
|
|
38
|
+
isNamespace?: boolean;
|
|
39
|
+
/** True when the import is `import('...')`. */
|
|
40
|
+
isDynamic?: boolean;
|
|
41
|
+
/** True when the import is `import '...'` (side-effect only). */
|
|
42
|
+
isSideEffect?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
26
45
|
* The result of building an import dependency tree.
|
|
27
46
|
*/
|
|
28
47
|
interface ImportTree {
|
|
@@ -34,14 +53,14 @@ interface ImportTree {
|
|
|
34
53
|
externals: string[];
|
|
35
54
|
/**
|
|
36
55
|
* Forward adjacency list: each key is an absolute file path, and its value
|
|
37
|
-
* is an array of
|
|
56
|
+
* is an array of import edges describing how it depends on other files.
|
|
38
57
|
*/
|
|
39
|
-
graph: Record<string,
|
|
58
|
+
graph: Record<string, ImportEdge[]>;
|
|
40
59
|
/**
|
|
41
60
|
* Reverse adjacency list: each key is an absolute file path, and its value
|
|
42
|
-
* is an array of
|
|
61
|
+
* is an array of import edges describing files that import it.
|
|
43
62
|
*/
|
|
44
|
-
reverseGraph: Record<string,
|
|
63
|
+
reverseGraph: Record<string, ImportEdge[]>;
|
|
45
64
|
}
|
|
46
65
|
//#endregion
|
|
47
66
|
//#region src/index.d.ts
|
|
@@ -64,6 +83,21 @@ interface ImportTree {
|
|
|
64
83
|
*/
|
|
65
84
|
declare function importree(entry: string, options?: ImportreeOptions): Promise<ImportTree>;
|
|
66
85
|
/**
|
|
86
|
+
* Parses a single file and returns its direct import edges without recursive traversal.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const edges = parseImports('./src/components/Button.tsx', {
|
|
91
|
+
* aliases: { '@': './src' },
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
94
|
+
* for (const edge of edges) {
|
|
95
|
+
* console.log(edge.path, edge.specifiers);
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
declare function parseImports(filePath: string, options?: ImportreeOptions): ImportEdge[];
|
|
100
|
+
/**
|
|
67
101
|
* Given an import tree and a changed file, returns all files that
|
|
68
102
|
* transitively depend on the changed file (i.e., files that would
|
|
69
103
|
* need to be re-evaluated if the changed file is modified).
|
|
@@ -72,5 +106,5 @@ declare function importree(entry: string, options?: ImportreeOptions): Promise<I
|
|
|
72
106
|
*/
|
|
73
107
|
declare function getAffectedFiles(tree: ImportTree, changedFile: string): string[];
|
|
74
108
|
//#endregion
|
|
75
|
-
export { type ImportTree, type ImportreeOptions, getAffectedFiles, importree };
|
|
109
|
+
export { type ImportEdge, type ImportTree, type ImportreeOptions, getAffectedFiles, importree, parseImports };
|
|
76
110
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";;AAGA;;UAAiB,gBAAA;EAaL;;;;EARV,OAAA;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";;AAGA;;UAAiB,gBAAA;EAaL;;;;EARV,OAAA;;;AAsBF;;;;EAdE,OAAA,GAAU,MAAA;;;;;;EAOV,UAAA;AAAA;;;;;UAOe,UAAA;;EAEf,IAAA;EAyCc;;;;EAnCd,UAAA;;EAGA,WAAA;;EAGA,SAAA;;EAGA,YAAA;AAAA;;;;UAMe,UAAA;EC3BK;ED6BpB,UAAA;;EAGA,KAAA;;EAGA,SAAA;ECnC0E;;;;EDyC1E,KAAA,EAAO,MAAA,SAAe,UAAA;;;;ACvBxB;ED6BE,YAAA,EAAc,MAAA,SAAe,UAAA;AAAA;;;;;;;;;;;;AA3C/B;;;;;;;;iBCJsB,SAAA,CAAU,KAAA,UAAe,OAAA,GAAU,gBAAA,GAAmB,OAAA,CAAQ,UAAA;;;AD2BpF;;;;;;;;;;;;iBCTgB,YAAA,CAAa,QAAA,UAAkB,OAAA,GAAU,gBAAA,GAAmB,UAAA;;;;;;;;iBAmB5D,gBAAA,CAAiB,IAAA,EAAM,UAAA,EAAY,WAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,100 +1,87 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
1
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { statSync } from "node:fs";
|
|
4
3
|
//#region src/scanner.ts
|
|
5
4
|
/**
|
|
6
5
|
* Strips comments from source code while preserving string literals.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Line comments (`//`) are removed entirely. Block comments are replaced
|
|
8
|
+
* with a single space (newlines within them are preserved). Strings and
|
|
9
9
|
* template literals are left intact so that import specifiers inside
|
|
10
10
|
* `from 'specifier'` remain extractable. The function correctly handles
|
|
11
11
|
* comment-like sequences inside strings (e.g., `'//'` won't start a comment).
|
|
12
12
|
*/
|
|
13
13
|
function stripComments(code) {
|
|
14
14
|
const len = code.length;
|
|
15
|
-
const
|
|
15
|
+
const parts = [];
|
|
16
16
|
let i = 0;
|
|
17
|
+
let segStart = 0;
|
|
17
18
|
while (i < len) {
|
|
18
19
|
const ch = code[i];
|
|
19
20
|
const next = i + 1 < len ? code[i + 1] : "";
|
|
20
21
|
if (ch === "/" && next === "/") {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
parts.push(code.slice(segStart, i));
|
|
23
|
+
while (i < len && code[i] !== "\n") i++;
|
|
24
|
+
segStart = i;
|
|
24
25
|
continue;
|
|
25
26
|
}
|
|
26
27
|
if (ch === "/" && next === "*") {
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
parts.push(code.slice(segStart, i));
|
|
29
|
+
i += 2;
|
|
29
30
|
while (i < len && !(code[i] === "*" && i + 1 < len && code[i + 1] === "/")) {
|
|
30
|
-
|
|
31
|
+
if (code[i] === "\n") parts.push("\n");
|
|
31
32
|
i++;
|
|
32
33
|
}
|
|
33
|
-
if (i < len)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
34
|
+
if (i < len) i += 2;
|
|
35
|
+
parts.push(" ");
|
|
36
|
+
segStart = i;
|
|
37
37
|
continue;
|
|
38
38
|
}
|
|
39
39
|
if (ch === "'" || ch === "\"") {
|
|
40
40
|
const quote = ch;
|
|
41
|
-
result[i] = code[i];
|
|
42
41
|
i++;
|
|
43
|
-
while (i < len && code[i] !== quote)
|
|
44
|
-
|
|
45
|
-
i++;
|
|
46
|
-
result[i] = code[i];
|
|
47
|
-
i++;
|
|
48
|
-
} else {
|
|
49
|
-
result[i] = code[i];
|
|
50
|
-
i++;
|
|
51
|
-
}
|
|
52
|
-
if (i < len) {
|
|
53
|
-
result[i] = code[i];
|
|
42
|
+
while (i < len && code[i] !== quote) {
|
|
43
|
+
if (code[i] === "\\" && i + 1 < len) i++;
|
|
54
44
|
i++;
|
|
55
45
|
}
|
|
46
|
+
if (i < len) i++;
|
|
56
47
|
continue;
|
|
57
48
|
}
|
|
58
49
|
if (ch === "`") {
|
|
59
|
-
result[i] = code[i];
|
|
60
50
|
i++;
|
|
61
51
|
let depth = 0;
|
|
62
|
-
while (i < len) if (code[i] === "\\" && i + 1 < len)
|
|
63
|
-
|
|
64
|
-
i
|
|
65
|
-
result[i] = code[i];
|
|
66
|
-
i++;
|
|
67
|
-
} else if (code[i] === "$" && i + 1 < len && code[i + 1] === "{") {
|
|
68
|
-
result[i] = code[i];
|
|
69
|
-
i++;
|
|
70
|
-
result[i] = code[i];
|
|
71
|
-
i++;
|
|
52
|
+
while (i < len) if (code[i] === "\\" && i + 1 < len) i += 2;
|
|
53
|
+
else if (code[i] === "$" && i + 1 < len && code[i + 1] === "{") {
|
|
54
|
+
i += 2;
|
|
72
55
|
depth++;
|
|
73
56
|
} else if (code[i] === "}" && depth > 0) {
|
|
74
|
-
result[i] = code[i];
|
|
75
57
|
i++;
|
|
76
58
|
depth--;
|
|
77
59
|
} else if (code[i] === "`" && depth === 0) {
|
|
78
|
-
result[i] = code[i];
|
|
79
60
|
i++;
|
|
80
61
|
break;
|
|
81
|
-
} else
|
|
82
|
-
result[i] = code[i];
|
|
83
|
-
i++;
|
|
84
|
-
}
|
|
62
|
+
} else i++;
|
|
85
63
|
continue;
|
|
86
64
|
}
|
|
87
|
-
result[i] = ch;
|
|
88
65
|
i++;
|
|
89
66
|
}
|
|
90
|
-
|
|
67
|
+
parts.push(code.slice(segStart));
|
|
68
|
+
return parts.join("");
|
|
91
69
|
}
|
|
92
|
-
const
|
|
70
|
+
const nsImportRe = /\bimport\s+\*\s+as\s+[$\w]+\s+from\s+['"]([^'"]+)['"]/g;
|
|
71
|
+
const namedImportRe = /\bimport\s+(?:type\s+)?(?:([$\w]+)\s*,\s*)?\{([^}]*)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
72
|
+
const defaultImportRe = /\bimport\s+(?:type\s+)?([$\w]+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
73
|
+
const reexportStarRe = /\bexport\s+\*\s+(?:as\s+[$\w]+\s+)?from\s+['"]([^'"]+)['"]/g;
|
|
74
|
+
const reexportNamedRe = /\bexport\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
93
75
|
const sideEffectRe = /\bimport\s+['"]([^'"]+)['"]/g;
|
|
94
76
|
const dynamicRe = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
95
77
|
const requireRe = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
78
|
+
function parseSpecifiers(clause) {
|
|
79
|
+
return clause.split(",").map((s) => s.trim()).filter(Boolean).map((s) => {
|
|
80
|
+
return s.replace(/^type\s+/, "").split(/\s+as\s+/)[0].trim();
|
|
81
|
+
}).filter(Boolean);
|
|
82
|
+
}
|
|
96
83
|
/**
|
|
97
|
-
* Scans source code and extracts all
|
|
84
|
+
* Scans source code and extracts all imports with metadata.
|
|
98
85
|
*
|
|
99
86
|
* Handles: static imports, dynamic imports, require(), re-exports.
|
|
100
87
|
* Ignores imports inside comments. Imports inside string literals may
|
|
@@ -103,12 +90,41 @@ const requireRe = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
|
103
90
|
*/
|
|
104
91
|
function scanImports(code) {
|
|
105
92
|
const stripped = stripComments(code);
|
|
106
|
-
const
|
|
107
|
-
for (const m of stripped.matchAll(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
93
|
+
const results = [];
|
|
94
|
+
for (const m of stripped.matchAll(nsImportRe)) results.push({
|
|
95
|
+
path: m[1],
|
|
96
|
+
isNamespace: true
|
|
97
|
+
});
|
|
98
|
+
for (const m of stripped.matchAll(namedImportRe)) {
|
|
99
|
+
const specifiers = parseSpecifiers(m[2]);
|
|
100
|
+
if (m[1]) specifiers.unshift("default");
|
|
101
|
+
results.push({
|
|
102
|
+
path: m[3],
|
|
103
|
+
specifiers
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
for (const m of stripped.matchAll(defaultImportRe)) results.push({
|
|
107
|
+
path: m[2],
|
|
108
|
+
specifiers: ["default"]
|
|
109
|
+
});
|
|
110
|
+
for (const m of stripped.matchAll(reexportStarRe)) results.push({
|
|
111
|
+
path: m[1],
|
|
112
|
+
isNamespace: true
|
|
113
|
+
});
|
|
114
|
+
for (const m of stripped.matchAll(reexportNamedRe)) results.push({
|
|
115
|
+
path: m[2],
|
|
116
|
+
specifiers: parseSpecifiers(m[1])
|
|
117
|
+
});
|
|
118
|
+
for (const m of stripped.matchAll(sideEffectRe)) results.push({
|
|
119
|
+
path: m[1],
|
|
120
|
+
isSideEffect: true
|
|
121
|
+
});
|
|
122
|
+
for (const m of stripped.matchAll(dynamicRe)) results.push({
|
|
123
|
+
path: m[1],
|
|
124
|
+
isDynamic: true
|
|
125
|
+
});
|
|
126
|
+
for (const m of stripped.matchAll(requireRe)) results.push({ path: m[1] });
|
|
127
|
+
return results;
|
|
112
128
|
}
|
|
113
129
|
//#endregion
|
|
114
130
|
//#region src/resolver.ts
|
|
@@ -192,9 +208,43 @@ function createResolver(basedir, options) {
|
|
|
192
208
|
}
|
|
193
209
|
//#endregion
|
|
194
210
|
//#region src/walker.ts
|
|
211
|
+
function buildEdges(rawImports, resolveSpecifier, filePath) {
|
|
212
|
+
const edges = [];
|
|
213
|
+
const externals = [];
|
|
214
|
+
for (const raw of rawImports) {
|
|
215
|
+
const resolved = resolveSpecifier(raw.path, filePath);
|
|
216
|
+
if (!resolved) continue;
|
|
217
|
+
if (resolved.type === "external" && resolved.specifier) externals.push(resolved.specifier);
|
|
218
|
+
else if (resolved.type === "local" && resolved.absolutePath) {
|
|
219
|
+
const existing = edges.find((e) => e.path === resolved.absolutePath);
|
|
220
|
+
if (existing) {
|
|
221
|
+
if (raw.isNamespace) {
|
|
222
|
+
existing.isNamespace = true;
|
|
223
|
+
existing.specifiers = void 0;
|
|
224
|
+
existing.isSideEffect = void 0;
|
|
225
|
+
} else if (raw.specifiers && !existing.isNamespace) {
|
|
226
|
+
(existing.specifiers ??= []).push(...raw.specifiers);
|
|
227
|
+
existing.isSideEffect = void 0;
|
|
228
|
+
}
|
|
229
|
+
if (raw.isSideEffect && !existing.specifiers && !existing.isNamespace) existing.isSideEffect = true;
|
|
230
|
+
if (raw.isDynamic) existing.isDynamic = true;
|
|
231
|
+
} else edges.push({
|
|
232
|
+
path: resolved.absolutePath,
|
|
233
|
+
specifiers: raw.specifiers,
|
|
234
|
+
isNamespace: raw.isNamespace,
|
|
235
|
+
isDynamic: raw.isDynamic,
|
|
236
|
+
isSideEffect: raw.isSideEffect
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
edges,
|
|
242
|
+
externals
|
|
243
|
+
};
|
|
244
|
+
}
|
|
195
245
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
246
|
+
* Walks imports starting from an entry file and builds the full dependency tree.
|
|
247
|
+
* Uses iterative DFS with an explicit stack to avoid call-stack limits on deep chains.
|
|
198
248
|
*/
|
|
199
249
|
async function walk(entryFile, options) {
|
|
200
250
|
const entrypoint = resolve(entryFile);
|
|
@@ -202,26 +252,27 @@ async function walk(entryFile, options) {
|
|
|
202
252
|
const graph = {};
|
|
203
253
|
const externals = /* @__PURE__ */ new Set();
|
|
204
254
|
const visited = /* @__PURE__ */ new Set();
|
|
205
|
-
|
|
206
|
-
|
|
255
|
+
const stack = [entrypoint];
|
|
256
|
+
while (stack.length > 0) {
|
|
257
|
+
const filePath = stack.pop();
|
|
258
|
+
if (visited.has(filePath)) continue;
|
|
207
259
|
visited.add(filePath);
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!resolved) continue;
|
|
213
|
-
if (resolved.type === "external" && resolved.specifier) externals.add(resolved.specifier);
|
|
214
|
-
else if (resolved.type === "local" && resolved.absolutePath) localDeps.push(resolved.absolutePath);
|
|
215
|
-
}
|
|
216
|
-
graph[filePath] = localDeps;
|
|
217
|
-
await Promise.all(localDeps.map((dep) => visit(dep)));
|
|
260
|
+
const { edges, externals: fileExternals } = buildEdges(scanImports(readFileSync(filePath, "utf-8")), resolveSpecifier, filePath);
|
|
261
|
+
for (const ext of fileExternals) externals.add(ext);
|
|
262
|
+
graph[filePath] = edges;
|
|
263
|
+
for (const edge of edges) stack.push(edge.path);
|
|
218
264
|
}
|
|
219
|
-
await visit(entrypoint);
|
|
220
265
|
const reverseGraph = {};
|
|
221
266
|
for (const file of Object.keys(graph)) reverseGraph[file] = [];
|
|
222
|
-
for (const [file,
|
|
223
|
-
if (!reverseGraph[
|
|
224
|
-
reverseGraph[
|
|
267
|
+
for (const [file, edges] of Object.entries(graph)) for (const edge of edges) {
|
|
268
|
+
if (!reverseGraph[edge.path]) reverseGraph[edge.path] = [];
|
|
269
|
+
reverseGraph[edge.path].push({
|
|
270
|
+
path: file,
|
|
271
|
+
specifiers: edge.specifiers,
|
|
272
|
+
isNamespace: edge.isNamespace,
|
|
273
|
+
isDynamic: edge.isDynamic,
|
|
274
|
+
isSideEffect: edge.isSideEffect
|
|
275
|
+
});
|
|
225
276
|
}
|
|
226
277
|
return {
|
|
227
278
|
entrypoint,
|
|
@@ -254,6 +305,26 @@ async function importree(entry, options) {
|
|
|
254
305
|
return walk(entry, options ?? {});
|
|
255
306
|
}
|
|
256
307
|
/**
|
|
308
|
+
* Parses a single file and returns its direct import edges without recursive traversal.
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```ts
|
|
312
|
+
* const edges = parseImports('./src/components/Button.tsx', {
|
|
313
|
+
* aliases: { '@': './src' },
|
|
314
|
+
* });
|
|
315
|
+
*
|
|
316
|
+
* for (const edge of edges) {
|
|
317
|
+
* console.log(edge.path, edge.specifiers);
|
|
318
|
+
* }
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
function parseImports(filePath, options) {
|
|
322
|
+
const absolutePath = resolve(filePath);
|
|
323
|
+
const resolveSpecifier = createResolver(options?.rootDir ? resolve(options.rootDir) : process.cwd(), options ?? {});
|
|
324
|
+
const { edges } = buildEdges(scanImports(readFileSync(absolutePath, "utf-8")), resolveSpecifier, absolutePath);
|
|
325
|
+
return edges;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
257
328
|
* Given an import tree and a changed file, returns all files that
|
|
258
329
|
* transitively depend on the changed file (i.e., files that would
|
|
259
330
|
* need to be re-evaluated if the changed file is modified).
|
|
@@ -269,15 +340,15 @@ function getAffectedFiles(tree, changedFile) {
|
|
|
269
340
|
const current = queue.shift();
|
|
270
341
|
const dependents = tree.reverseGraph[current];
|
|
271
342
|
if (!dependents) continue;
|
|
272
|
-
for (const
|
|
273
|
-
affected.add(
|
|
274
|
-
queue.push(
|
|
343
|
+
for (const edge of dependents) if (!affected.has(edge.path)) {
|
|
344
|
+
affected.add(edge.path);
|
|
345
|
+
queue.push(edge.path);
|
|
275
346
|
}
|
|
276
347
|
}
|
|
277
348
|
affected.delete(absolute);
|
|
278
349
|
return [...affected].sort();
|
|
279
350
|
}
|
|
280
351
|
//#endregion
|
|
281
|
-
export { getAffectedFiles, importree };
|
|
352
|
+
export { getAffectedFiles, importree, parseImports };
|
|
282
353
|
|
|
283
354
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/scanner.ts","../src/resolver.ts","../src/walker.ts","../src/index.ts"],"sourcesContent":["/**\n * Strips comments from source code while preserving string literals.\n *\n * Comments are replaced with spaces (preserving newlines). Strings and\n * template literals are left intact so that import specifiers inside\n * `from 'specifier'` remain extractable. The function correctly handles\n * comment-like sequences inside strings (e.g., `'//'` won't start a comment).\n */\nexport function stripComments(code: string): string {\n const len = code.length;\n const result: string[] = new Array(len);\n let i = 0;\n\n while (i < len) {\n const ch = code[i];\n const next = i + 1 < len ? code[i + 1] : '';\n\n // Line comment → blank to end of line\n if (ch === '/' && next === '/') {\n result[i++] = ' ';\n result[i++] = ' ';\n while (i < len && code[i] !== '\\n') {\n result[i++] = ' ';\n }\n continue;\n }\n\n // Block comment → blank to closing */\n if (ch === '/' && next === '*') {\n result[i++] = ' ';\n result[i++] = ' ';\n while (i < len && !(code[i] === '*' && i + 1 < len && code[i + 1] === '/')) {\n result[i] = code[i] === '\\n' ? '\\n' : ' ';\n i++;\n }\n if (i < len) {\n result[i++] = ' '; // *\n result[i++] = ' '; // /\n }\n continue;\n }\n\n // Single or double quoted string — copy verbatim (skip past to avoid\n // misidentifying comment markers inside strings)\n if (ch === \"'\" || ch === '\"') {\n const quote = ch;\n result[i] = code[i];\n i++;\n while (i < len && code[i] !== quote) {\n if (code[i] === '\\\\' && i + 1 < len) {\n result[i] = code[i];\n i++;\n result[i] = code[i];\n i++;\n } else {\n result[i] = code[i];\n i++;\n }\n }\n if (i < len) {\n result[i] = code[i];\n i++;\n }\n continue;\n }\n\n // Template literal — copy verbatim, handling ${} nesting\n if (ch === '`') {\n result[i] = code[i];\n i++;\n let depth = 0;\n while (i < len) {\n if (code[i] === '\\\\' && i + 1 < len) {\n result[i] = code[i];\n i++;\n result[i] = code[i];\n i++;\n } else if (code[i] === '$' && i + 1 < len && code[i + 1] === '{') {\n result[i] = code[i];\n i++;\n result[i] = code[i];\n i++;\n depth++;\n } else if (code[i] === '}' && depth > 0) {\n result[i] = code[i];\n i++;\n depth--;\n } else if (code[i] === '`' && depth === 0) {\n result[i] = code[i];\n i++;\n break;\n } else {\n result[i] = code[i];\n i++;\n }\n }\n continue;\n }\n\n // Regular character\n result[i] = ch;\n i++;\n }\n\n return result.join('');\n}\n\n// Static regex patterns — compiled once\nconst fromRe = /\\bfrom\\s+['\"]([^'\"]+)['\"]/g;\nconst sideEffectRe = /\\bimport\\s+['\"]([^'\"]+)['\"]/g;\nconst dynamicRe = /\\bimport\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\nconst requireRe = /\\brequire\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n\n/**\n * Scans source code and extracts all import/require specifiers.\n *\n * Handles: static imports, dynamic imports, require(), re-exports.\n * Ignores imports inside comments. Imports inside string literals may\n * produce false positives, but unresolvable paths are silently skipped\n * by the resolver.\n */\nexport function scanImports(code: string): string[] {\n const stripped = stripComments(code);\n const specifiers = new Set<string>();\n\n for (const m of stripped.matchAll(fromRe)) specifiers.add(m[1]);\n for (const m of stripped.matchAll(sideEffectRe)) specifiers.add(m[1]);\n for (const m of stripped.matchAll(dynamicRe)) specifiers.add(m[1]);\n for (const m of stripped.matchAll(requireRe)) specifiers.add(m[1]);\n\n return [...specifiers];\n}\n","import { statSync } from 'node:fs';\nimport { dirname, join, resolve, isAbsolute } from 'node:path';\nimport type { ImportreeOptions, ResolvedImport } from './types.js';\n\nconst DEFAULT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];\n\nfunction fileExists(filePath: string): boolean {\n const stat = statSync(filePath, { throwIfNoEntry: false });\n return stat !== undefined && stat.isFile();\n}\n\nfunction dirExists(filePath: string): boolean {\n const stat = statSync(filePath, { throwIfNoEntry: false });\n return stat !== undefined && stat.isDirectory();\n}\n\n/**\n * Extract the bare package name from an import specifier.\n * - Scoped: `@scope/pkg/path` → `@scope/pkg`\n * - Unscoped: `pkg/path` → `pkg`\n */\nfunction getBareSpecifier(specifier: string): string {\n if (specifier.startsWith('@')) {\n const parts = specifier.split('/');\n return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;\n }\n return specifier.split('/')[0];\n}\n\nfunction resolveFile(\n filePath: string,\n extensions: string[],\n): string | undefined {\n // Try exact path\n if (fileExists(filePath)) return filePath;\n\n // Try with each extension\n for (const ext of extensions) {\n const withExt = filePath + ext;\n if (fileExists(withExt)) return withExt;\n }\n\n // Try as directory with index file\n if (dirExists(filePath)) {\n for (const ext of extensions) {\n const indexPath = join(filePath, `index${ext}`);\n if (fileExists(indexPath)) return indexPath;\n }\n }\n\n return undefined;\n}\n\nexport interface Resolver {\n (specifier: string, fromFile: string): ResolvedImport | undefined;\n}\n\n/**\n * Creates a resolver function that resolves import specifiers to absolute\n * file paths, with support for aliases and extension probing.\n */\nexport function createResolver(\n basedir: string,\n options: ImportreeOptions,\n): Resolver {\n const extensions = options.extensions ?? DEFAULT_EXTENSIONS;\n\n // Sort aliases by key length descending for longest-prefix matching\n const aliases = options.aliases\n ? Object.entries(options.aliases).sort((a, b) => b[0].length - a[0].length)\n : [];\n\n const resolvedAliasValues = aliases.map(([key, value]) => [\n key,\n isAbsolute(value) ? value : resolve(basedir, value),\n ] as const);\n\n const cache = new Map<string, ResolvedImport | undefined>();\n\n return function resolveSpecifier(\n specifier: string,\n fromFile: string,\n ): ResolvedImport | undefined {\n const fromDir = dirname(fromFile);\n const cacheKey = `${specifier}\\0${fromDir}`;\n\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n\n let result: ResolvedImport | undefined;\n\n // Relative import\n if (specifier.startsWith('./') || specifier.startsWith('../')) {\n const absolutePath = resolveFile(resolve(fromDir, specifier), extensions);\n if (absolutePath) {\n result = { type: 'local', absolutePath };\n }\n }\n // Check aliases\n else {\n let matched = false;\n for (const [prefix, replacement] of resolvedAliasValues) {\n if (specifier === prefix || specifier.startsWith(prefix + '/')) {\n const rest = specifier === prefix ? '' : specifier.slice(prefix.length);\n const absolutePath = resolveFile(join(replacement, rest), extensions);\n if (absolutePath) {\n result = { type: 'local', absolutePath };\n }\n matched = true;\n break;\n }\n }\n\n // Bare specifier → external\n if (!matched) {\n result = { type: 'external', specifier: getBareSpecifier(specifier) };\n }\n }\n\n cache.set(cacheKey, result);\n return result;\n };\n}\n","import { readFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport type { ImportreeOptions, ImportTree } from './types.js';\nimport { scanImports } from './scanner.js';\nimport { createResolver } from './resolver.js';\n\n/**\n * Recursively walks imports starting from an entry file and builds\n * the full dependency tree.\n */\nexport async function walk(\n entryFile: string,\n options: ImportreeOptions,\n): Promise<ImportTree> {\n const entrypoint = resolve(entryFile);\n const basedir = options.rootDir ? resolve(options.rootDir) : process.cwd();\n const resolveSpecifier = createResolver(basedir, options);\n\n const graph: Record<string, string[]> = {};\n const externals = new Set<string>();\n const visited = new Set<string>();\n\n async function visit(filePath: string): Promise<void> {\n if (visited.has(filePath)) return;\n visited.add(filePath);\n\n const content = await readFile(filePath, 'utf-8');\n const specifiers = scanImports(content);\n\n const localDeps: string[] = [];\n for (const spec of specifiers) {\n const resolved = resolveSpecifier(spec, filePath);\n if (!resolved) continue;\n\n if (resolved.type === 'external' && resolved.specifier) {\n externals.add(resolved.specifier);\n } else if (resolved.type === 'local' && resolved.absolutePath) {\n localDeps.push(resolved.absolutePath);\n }\n }\n\n graph[filePath] = localDeps;\n\n await Promise.all(localDeps.map((dep) => visit(dep)));\n }\n\n await visit(entrypoint);\n\n // Build reverse graph\n const reverseGraph: Record<string, string[]> = {};\n for (const file of Object.keys(graph)) {\n reverseGraph[file] = [];\n }\n for (const [file, deps] of Object.entries(graph)) {\n for (const dep of deps) {\n if (!reverseGraph[dep]) reverseGraph[dep] = [];\n reverseGraph[dep].push(file);\n }\n }\n\n return {\n entrypoint,\n files: Object.keys(graph).sort(),\n externals: [...externals].sort(),\n graph,\n reverseGraph,\n };\n}\n","import { resolve } from 'node:path';\nimport type { ImportreeOptions, ImportTree } from './types.js';\nimport { walk } from './walker.js';\n\nexport type { ImportreeOptions, ImportTree } from './types.js';\n\n/**\n * Builds a full import dependency tree starting from an entry file.\n *\n * Recursively resolves all static imports, dynamic imports, require() calls,\n * and re-exports. Supports path aliases for custom resolution.\n *\n * @example\n * ```ts\n * const tree = await importree('./src/index.ts', {\n * aliases: { '@': './src' },\n * });\n *\n * console.log(tree.files); // all local dependency file paths\n * console.log(tree.externals); // external package names\n * console.log(tree.graph); // file → direct dependencies\n * ```\n */\nexport async function importree(\n entry: string,\n options?: ImportreeOptions,\n): Promise<ImportTree> {\n return walk(entry, options ?? {});\n}\n\n/**\n * Given an import tree and a changed file, returns all files that\n * transitively depend on the changed file (i.e., files that would\n * need to be re-evaluated if the changed file is modified).\n *\n * The changed file itself is NOT included in the result.\n */\nexport function getAffectedFiles(\n tree: ImportTree,\n changedFile: string,\n): string[] {\n const absolute = resolve(changedFile);\n\n if (!tree.reverseGraph[absolute]) return [];\n\n const affected = new Set<string>();\n const queue = [absolute];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const dependents = tree.reverseGraph[current];\n if (!dependents) continue;\n\n for (const parent of dependents) {\n if (!affected.has(parent)) {\n affected.add(parent);\n queue.push(parent);\n }\n }\n }\n\n affected.delete(absolute);\n return [...affected].sort();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/scanner.ts","../src/resolver.ts","../src/walker.ts","../src/index.ts"],"sourcesContent":["/**\n * Strips comments from source code while preserving string literals.\n *\n * Line comments (`//`) are removed entirely. Block comments are replaced\n * with a single space (newlines within them are preserved). Strings and\n * template literals are left intact so that import specifiers inside\n * `from 'specifier'` remain extractable. The function correctly handles\n * comment-like sequences inside strings (e.g., `'//'` won't start a comment).\n */\nexport function stripComments(code: string): string {\n const len = code.length;\n const parts: string[] = [];\n let i = 0;\n let segStart = 0;\n\n while (i < len) {\n const ch = code[i];\n const next = i + 1 < len ? code[i + 1] : \"\";\n\n if (ch === \"/\" && next === \"/\") {\n parts.push(code.slice(segStart, i));\n while (i < len && code[i] !== \"\\n\") i++;\n segStart = i;\n continue;\n }\n\n if (ch === \"/\" && next === \"*\") {\n parts.push(code.slice(segStart, i));\n i += 2;\n while (i < len && !(code[i] === \"*\" && i + 1 < len && code[i + 1] === \"/\")) {\n if (code[i] === \"\\n\") parts.push(\"\\n\");\n i++;\n }\n if (i < len) i += 2;\n parts.push(\" \");\n segStart = i;\n continue;\n }\n\n if (ch === \"'\" || ch === '\"') {\n const quote = ch;\n i++;\n while (i < len && code[i] !== quote) {\n if (code[i] === \"\\\\\" && i + 1 < len) i++;\n i++;\n }\n if (i < len) i++;\n continue;\n }\n\n if (ch === \"`\") {\n i++;\n let depth = 0;\n while (i < len) {\n if (code[i] === \"\\\\\" && i + 1 < len) {\n i += 2;\n } else if (code[i] === \"$\" && i + 1 < len && code[i + 1] === \"{\") {\n i += 2;\n depth++;\n } else if (code[i] === \"}\" && depth > 0) {\n i++;\n depth--;\n } else if (code[i] === \"`\" && depth === 0) {\n i++;\n break;\n } else {\n i++;\n }\n }\n continue;\n }\n\n i++;\n }\n\n parts.push(code.slice(segStart));\n return parts.join(\"\");\n}\n\nexport interface RawImport {\n path: string;\n specifiers?: string[];\n isNamespace?: boolean;\n isDynamic?: boolean;\n isSideEffect?: boolean;\n}\n\n// Static regex patterns — compiled once\nconst nsImportRe = /\\bimport\\s+\\*\\s+as\\s+[$\\w]+\\s+from\\s+['\"]([^'\"]+)['\"]/g;\nconst namedImportRe =\n /\\bimport\\s+(?:type\\s+)?(?:([$\\w]+)\\s*,\\s*)?\\{([^}]*)\\}\\s+from\\s+['\"]([^'\"]+)['\"]/g;\nconst defaultImportRe = /\\bimport\\s+(?:type\\s+)?([$\\w]+)\\s+from\\s+['\"]([^'\"]+)['\"]/g;\nconst reexportStarRe = /\\bexport\\s+\\*\\s+(?:as\\s+[$\\w]+\\s+)?from\\s+['\"]([^'\"]+)['\"]/g;\nconst reexportNamedRe = /\\bexport\\s+(?:type\\s+)?\\{([^}]*)\\}\\s+from\\s+['\"]([^'\"]+)['\"]/g;\nconst sideEffectRe = /\\bimport\\s+['\"]([^'\"]+)['\"]/g;\nconst dynamicRe = /\\bimport\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\nconst requireRe = /\\brequire\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n\nexport function parseSpecifiers(clause: string): string[] {\n return clause\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean)\n .map((s) => {\n const withoutType = s.replace(/^type\\s+/, \"\");\n const parts = withoutType.split(/\\s+as\\s+/);\n return parts[0].trim();\n })\n .filter(Boolean);\n}\n\n/**\n * Scans source code and extracts all imports with metadata.\n *\n * Handles: static imports, dynamic imports, require(), re-exports.\n * Ignores imports inside comments. Imports inside string literals may\n * produce false positives, but unresolvable paths are silently skipped\n * by the resolver.\n */\nexport function scanImports(code: string): RawImport[] {\n const stripped = stripComments(code);\n const results: RawImport[] = [];\n\n for (const m of stripped.matchAll(nsImportRe)) {\n results.push({ path: m[1], isNamespace: true });\n }\n\n for (const m of stripped.matchAll(namedImportRe)) {\n const specifiers = parseSpecifiers(m[2]);\n if (m[1]) specifiers.unshift(\"default\");\n results.push({ path: m[3], specifiers });\n }\n\n for (const m of stripped.matchAll(defaultImportRe)) {\n results.push({ path: m[2], specifiers: [\"default\"] });\n }\n\n for (const m of stripped.matchAll(reexportStarRe)) {\n results.push({ path: m[1], isNamespace: true });\n }\n\n for (const m of stripped.matchAll(reexportNamedRe)) {\n results.push({ path: m[2], specifiers: parseSpecifiers(m[1]) });\n }\n\n for (const m of stripped.matchAll(sideEffectRe)) {\n results.push({ path: m[1], isSideEffect: true });\n }\n\n for (const m of stripped.matchAll(dynamicRe)) {\n results.push({ path: m[1], isDynamic: true });\n }\n\n for (const m of stripped.matchAll(requireRe)) {\n results.push({ path: m[1] });\n }\n\n return results;\n}\n","import { statSync } from \"node:fs\";\nimport { dirname, join, resolve, isAbsolute } from \"node:path\";\nimport type { ImportreeOptions, ResolvedImport } from \"./types.js\";\n\nconst DEFAULT_EXTENSIONS = [\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\", \".cjs\"];\n\nfunction fileExists(filePath: string): boolean {\n const stat = statSync(filePath, { throwIfNoEntry: false });\n return stat !== undefined && stat.isFile();\n}\n\nfunction dirExists(filePath: string): boolean {\n const stat = statSync(filePath, { throwIfNoEntry: false });\n return stat !== undefined && stat.isDirectory();\n}\n\n/**\n * Extract the bare package name from an import specifier.\n * - Scoped: `@scope/pkg/path` → `@scope/pkg`\n * - Unscoped: `pkg/path` → `pkg`\n */\nfunction getBareSpecifier(specifier: string): string {\n if (specifier.startsWith(\"@\")) {\n const parts = specifier.split(\"/\");\n return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;\n }\n return specifier.split(\"/\")[0];\n}\n\nfunction resolveFile(filePath: string, extensions: string[]): string | undefined {\n // Try exact path\n if (fileExists(filePath)) return filePath;\n\n // Try with each extension\n for (const ext of extensions) {\n const withExt = filePath + ext;\n if (fileExists(withExt)) return withExt;\n }\n\n // Try as directory with index file\n if (dirExists(filePath)) {\n for (const ext of extensions) {\n const indexPath = join(filePath, `index${ext}`);\n if (fileExists(indexPath)) return indexPath;\n }\n }\n\n return undefined;\n}\n\nexport interface Resolver {\n (specifier: string, fromFile: string): ResolvedImport | undefined;\n}\n\n/**\n * Creates a resolver function that resolves import specifiers to absolute\n * file paths, with support for aliases and extension probing.\n */\nexport function createResolver(basedir: string, options: ImportreeOptions): Resolver {\n const extensions = options.extensions ?? DEFAULT_EXTENSIONS;\n\n // Sort aliases by key length descending for longest-prefix matching\n const aliases = options.aliases\n ? Object.entries(options.aliases).sort((a, b) => b[0].length - a[0].length)\n : [];\n\n const resolvedAliasValues = aliases.map(\n ([key, value]) => [key, isAbsolute(value) ? value : resolve(basedir, value)] as const,\n );\n\n const cache = new Map<string, ResolvedImport | undefined>();\n\n return function resolveSpecifier(\n specifier: string,\n fromFile: string,\n ): ResolvedImport | undefined {\n const fromDir = dirname(fromFile);\n const cacheKey = `${specifier}\\0${fromDir}`;\n\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n\n let result: ResolvedImport | undefined;\n\n // Relative import\n if (specifier.startsWith(\"./\") || specifier.startsWith(\"../\")) {\n const absolutePath = resolveFile(resolve(fromDir, specifier), extensions);\n if (absolutePath) {\n result = { type: \"local\", absolutePath };\n }\n }\n // Check aliases\n else {\n let matched = false;\n for (const [prefix, replacement] of resolvedAliasValues) {\n if (specifier === prefix || specifier.startsWith(prefix + \"/\")) {\n const rest = specifier === prefix ? \"\" : specifier.slice(prefix.length);\n const absolutePath = resolveFile(join(replacement, rest), extensions);\n if (absolutePath) {\n result = { type: \"local\", absolutePath };\n }\n matched = true;\n break;\n }\n }\n\n // Bare specifier → external\n if (!matched) {\n result = { type: \"external\", specifier: getBareSpecifier(specifier) };\n }\n }\n\n cache.set(cacheKey, result);\n return result;\n };\n}\n","import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { ImportreeOptions, ImportTree, ImportEdge } from \"./types.js\";\nimport type { RawImport } from \"./scanner.js\";\nimport { scanImports } from \"./scanner.js\";\nimport type { Resolver } from \"./resolver.js\";\nimport { createResolver } from \"./resolver.js\";\n\nexport function buildEdges(\n rawImports: RawImport[],\n resolveSpecifier: Resolver,\n filePath: string,\n): { edges: ImportEdge[]; externals: string[] } {\n const edges: ImportEdge[] = [];\n const externals: string[] = [];\n\n for (const raw of rawImports) {\n const resolved = resolveSpecifier(raw.path, filePath);\n if (!resolved) continue;\n\n if (resolved.type === \"external\" && resolved.specifier) {\n externals.push(resolved.specifier);\n } else if (resolved.type === \"local\" && resolved.absolutePath) {\n const existing = edges.find((e) => e.path === resolved.absolutePath);\n if (existing) {\n if (raw.isNamespace) {\n existing.isNamespace = true;\n existing.specifiers = undefined;\n existing.isSideEffect = undefined;\n } else if (raw.specifiers && !existing.isNamespace) {\n (existing.specifiers ??= []).push(...raw.specifiers);\n existing.isSideEffect = undefined;\n }\n if (raw.isSideEffect && !existing.specifiers && !existing.isNamespace) {\n existing.isSideEffect = true;\n }\n if (raw.isDynamic) existing.isDynamic = true;\n } else {\n edges.push({\n path: resolved.absolutePath,\n specifiers: raw.specifiers,\n isNamespace: raw.isNamespace,\n isDynamic: raw.isDynamic,\n isSideEffect: raw.isSideEffect,\n });\n }\n }\n }\n\n return { edges, externals };\n}\n\n/**\n * Walks imports starting from an entry file and builds the full dependency tree.\n * Uses iterative DFS with an explicit stack to avoid call-stack limits on deep chains.\n */\nexport async function walk(entryFile: string, options: ImportreeOptions): Promise<ImportTree> {\n const entrypoint = resolve(entryFile);\n const basedir = options.rootDir ? resolve(options.rootDir) : process.cwd();\n const resolveSpecifier = createResolver(basedir, options);\n\n const graph: Record<string, ImportEdge[]> = {};\n const externals = new Set<string>();\n const visited = new Set<string>();\n const stack = [entrypoint];\n\n while (stack.length > 0) {\n const filePath = stack.pop()!;\n if (visited.has(filePath)) continue;\n visited.add(filePath);\n\n const content = readFileSync(filePath, \"utf-8\");\n const rawImports = scanImports(content);\n const { edges, externals: fileExternals } = buildEdges(rawImports, resolveSpecifier, filePath);\n\n for (const ext of fileExternals) externals.add(ext);\n graph[filePath] = edges;\n\n for (const edge of edges) stack.push(edge.path);\n }\n\n // Build reverse graph\n const reverseGraph: Record<string, ImportEdge[]> = {};\n for (const file of Object.keys(graph)) {\n reverseGraph[file] = [];\n }\n for (const [file, edges] of Object.entries(graph)) {\n for (const edge of edges) {\n if (!reverseGraph[edge.path]) reverseGraph[edge.path] = [];\n reverseGraph[edge.path].push({\n path: file,\n specifiers: edge.specifiers,\n isNamespace: edge.isNamespace,\n isDynamic: edge.isDynamic,\n isSideEffect: edge.isSideEffect,\n });\n }\n }\n\n return {\n entrypoint,\n files: Object.keys(graph).sort(),\n externals: [...externals].sort(),\n graph,\n reverseGraph,\n };\n}\n","import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { ImportreeOptions, ImportTree, ImportEdge } from \"./types.js\";\nimport { buildEdges, walk } from \"./walker.js\";\nimport { scanImports } from \"./scanner.js\";\nimport { createResolver } from \"./resolver.js\";\n\nexport type { ImportreeOptions, ImportTree, ImportEdge } from \"./types.js\";\n\n/**\n * Builds a full import dependency tree starting from an entry file.\n *\n * Recursively resolves all static imports, dynamic imports, require() calls,\n * and re-exports. Supports path aliases for custom resolution.\n *\n * @example\n * ```ts\n * const tree = await importree('./src/index.ts', {\n * aliases: { '@': './src' },\n * });\n *\n * console.log(tree.files); // all local dependency file paths\n * console.log(tree.externals); // external package names\n * console.log(tree.graph); // file → direct dependencies\n * ```\n */\nexport async function importree(entry: string, options?: ImportreeOptions): Promise<ImportTree> {\n return walk(entry, options ?? {});\n}\n\n/**\n * Parses a single file and returns its direct import edges without recursive traversal.\n *\n * @example\n * ```ts\n * const edges = parseImports('./src/components/Button.tsx', {\n * aliases: { '@': './src' },\n * });\n *\n * for (const edge of edges) {\n * console.log(edge.path, edge.specifiers);\n * }\n * ```\n */\nexport function parseImports(filePath: string, options?: ImportreeOptions): ImportEdge[] {\n const absolutePath = resolve(filePath);\n const basedir = options?.rootDir ? resolve(options.rootDir) : process.cwd();\n const resolveSpecifier = createResolver(basedir, options ?? {});\n\n const content = readFileSync(absolutePath, \"utf-8\");\n const rawImports = scanImports(content);\n const { edges } = buildEdges(rawImports, resolveSpecifier, absolutePath);\n\n return edges;\n}\n\n/**\n * Given an import tree and a changed file, returns all files that\n * transitively depend on the changed file (i.e., files that would\n * need to be re-evaluated if the changed file is modified).\n *\n * The changed file itself is NOT included in the result.\n */\nexport function getAffectedFiles(tree: ImportTree, changedFile: string): string[] {\n const absolute = resolve(changedFile);\n\n if (!tree.reverseGraph[absolute]) return [];\n\n const affected = new Set<string>();\n const queue = [absolute];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const dependents = tree.reverseGraph[current];\n if (!dependents) continue;\n\n for (const edge of dependents) {\n if (!affected.has(edge.path)) {\n affected.add(edge.path);\n queue.push(edge.path);\n }\n }\n }\n\n affected.delete(absolute);\n return [...affected].sort();\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "importree",
|
|
3
|
-
"version": "
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "2.0.1",
|
|
5
4
|
"description": "Build import dependency trees for TypeScript and JavaScript files. Fast, zero-dependency static analysis for dependency detection and cache invalidation.",
|
|
6
|
-
"author": "Alex Grozav <alex@grozav.com>",
|
|
7
|
-
"license": "ISC",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/alexgrozav/importree.git"
|
|
11
|
-
},
|
|
12
|
-
"homepage": "https://importree.js.org",
|
|
13
|
-
"bugs": {
|
|
14
|
-
"url": "https://github.com/alexgrozav/importree/issues"
|
|
15
|
-
},
|
|
16
5
|
"keywords": [
|
|
17
|
-
"
|
|
6
|
+
"cache-invalidation",
|
|
18
7
|
"dependencies",
|
|
19
8
|
"dependency-graph",
|
|
20
9
|
"import-tree",
|
|
10
|
+
"imports",
|
|
11
|
+
"javascript",
|
|
21
12
|
"static-analysis",
|
|
22
|
-
"
|
|
23
|
-
"typescript",
|
|
24
|
-
"javascript"
|
|
13
|
+
"typescript"
|
|
25
14
|
],
|
|
26
|
-
"
|
|
27
|
-
|
|
15
|
+
"homepage": "https://importree.js.org",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/alexgrozav/importree/issues"
|
|
28
18
|
},
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"author": "Alex Grozav <alex@grozav.com>",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/alexgrozav/importree.git"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
29
|
"main": "./dist/index.cjs",
|
|
30
30
|
"module": "./dist/index.mjs",
|
|
31
31
|
"types": "./dist/index.d.mts",
|
|
@@ -41,17 +41,20 @@
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
|
-
"files": [
|
|
45
|
-
"dist"
|
|
46
|
-
],
|
|
47
44
|
"devDependencies": {
|
|
45
|
+
"@types/node": "^25.3.5",
|
|
48
46
|
"@vitest/coverage-v8": "^4.0.18",
|
|
49
47
|
"dependency-tree": "^11.4.0",
|
|
50
48
|
"madge": "^8.0.0",
|
|
49
|
+
"oxfmt": "^0.36.0",
|
|
50
|
+
"oxlint": "^1.51.0",
|
|
51
51
|
"tsdown": "^0.21.0",
|
|
52
52
|
"typescript": "^5.7.0",
|
|
53
53
|
"vitest": "^4.0.18"
|
|
54
54
|
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18"
|
|
57
|
+
},
|
|
55
58
|
"scripts": {
|
|
56
59
|
"build": "tsdown",
|
|
57
60
|
"build:docs": "cp -r docs dist-docs",
|
|
@@ -60,7 +63,13 @@
|
|
|
60
63
|
"dev:docs": "npx http-server docs -p 8765 -o",
|
|
61
64
|
"bench": "vitest bench",
|
|
62
65
|
"bench:run": "vitest bench --run",
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"lint": "oxlint",
|
|
68
|
+
"lint:fix": "oxlint --fix",
|
|
69
|
+
"fmt": "oxfmt",
|
|
70
|
+
"fmt:check": "oxfmt --check",
|
|
63
71
|
"test": "vitest run",
|
|
72
|
+
"test:coverage": "vitest run --coverage",
|
|
64
73
|
"test:watch": "vitest"
|
|
65
74
|
}
|
|
66
75
|
}
|