importree 1.0.0 → 1.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 +34 -1
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ When a file changes, you need to know what else is affected. importree builds th
|
|
|
11
11
|
|
|
12
12
|
Built for CI pipelines, build tools, monorepo task runners, and test selectors.
|
|
13
13
|
|
|
14
|
-
[Website](https://
|
|
14
|
+
[Website](https://importree.js.org) · [GitHub](https://github.com/alexgrozav/importree) · [npm](https://www.npmjs.com/package/importree)
|
|
15
15
|
|
|
16
16
|
## Highlights
|
|
17
17
|
|
|
@@ -21,6 +21,39 @@ Built for CI pipelines, build tools, monorepo task runners, and test selectors.
|
|
|
21
21
|
- **Cache invalidation** — Pre-computed reverse dependency graph answers "what needs rebuilding?" instantly.
|
|
22
22
|
- **Dual output** — Ships both ESM and CJS with full TypeScript declarations.
|
|
23
23
|
|
|
24
|
+
## Benchmarks
|
|
25
|
+
|
|
26
|
+
Measured with [Vitest bench](https://vitest.dev/guide/features.html#benchmarking) on Node.js v22. Results vary by hardware.
|
|
27
|
+
|
|
28
|
+
### Comparison with alternatives
|
|
29
|
+
|
|
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
|
+
|
|
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.4 ms** | 3.1 ms | 3.7 ms | 0.6 ms | 49.9 ms |
|
|
35
|
+
| Medium (100 files) | **2.1 ms** | 14.3 ms | 15.1 ms | 5.2 ms | 48.4 ms |
|
|
36
|
+
| Large (500 files) | **12.7 ms** | 44.4 ms | 43.3 ms | 26.5 ms | 50.9 ms |
|
|
37
|
+
|
|
38
|
+
### Full tree build
|
|
39
|
+
|
|
40
|
+
| Project size | Mean time | Throughput |
|
|
41
|
+
|-------------|-----------|------------|
|
|
42
|
+
| 10 files | 0.4 ms | ~2,548 ops/s |
|
|
43
|
+
| 100 files | 2.5 ms | ~406 ops/s |
|
|
44
|
+
| 500 files | 12.1 ms | ~83 ops/s |
|
|
45
|
+
| 1,000 files | 26.4 ms | ~38 ops/s |
|
|
46
|
+
|
|
47
|
+
### Scanner throughput
|
|
48
|
+
|
|
49
|
+
| Operation | Throughput |
|
|
50
|
+
|-----------|-----------|
|
|
51
|
+
| `scanImports` (3 imports) | ~661K ops/s |
|
|
52
|
+
| `scanImports` (50 imports) | ~41K ops/s |
|
|
53
|
+
| `stripComments` (1,000 lines) | ~2,497 ops/s |
|
|
54
|
+
|
|
55
|
+
> Run `pnpm bench:run` to reproduce locally.
|
|
56
|
+
|
|
24
57
|
## Install
|
|
25
58
|
|
|
26
59
|
```sh
|
package/dist/index.cjs
CHANGED
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 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 * 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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"}
|
package/dist/index.mjs
CHANGED
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 return [...affected].sort();\n}\n"],"mappings":";;;;;;;;;;;;AAQA,SAAgB,cAAc,MAAsB;CAClD,MAAM,MAAM,KAAK;CACjB,MAAM,SAAmB,IAAI,MAAM,IAAI;CACvC,IAAI,IAAI;AAER,QAAO,IAAI,KAAK;EACd,MAAM,KAAK,KAAK;EAChB,MAAM,OAAO,IAAI,IAAI,MAAM,KAAK,IAAI,KAAK;AAGzC,MAAI,OAAO,OAAO,SAAS,KAAK;AAC9B,UAAO,OAAO;AACd,UAAO,OAAO;AACd,UAAO,IAAI,OAAO,KAAK,OAAO,KAC5B,QAAO,OAAO;AAEhB;;AAIF,MAAI,OAAO,OAAO,SAAS,KAAK;AAC9B,UAAO,OAAO;AACd,UAAO,OAAO;AACd,UAAO,IAAI,OAAO,EAAE,KAAK,OAAO,OAAO,IAAI,IAAI,OAAO,KAAK,IAAI,OAAO,MAAM;AAC1E,WAAO,KAAK,KAAK,OAAO,OAAO,OAAO;AACtC;;AAEF,OAAI,IAAI,KAAK;AACX,WAAO,OAAO;AACd,WAAO,OAAO;;AAEhB;;AAKF,MAAI,OAAO,OAAO,OAAO,MAAK;GAC5B,MAAM,QAAQ;AACd,UAAO,KAAK,KAAK;AACjB;AACA,UAAO,IAAI,OAAO,KAAK,OAAO,MAC5B,KAAI,KAAK,OAAO,QAAQ,IAAI,IAAI,KAAK;AACnC,WAAO,KAAK,KAAK;AACjB;AACA,WAAO,KAAK,KAAK;AACjB;UACK;AACL,WAAO,KAAK,KAAK;AACjB;;AAGJ,OAAI,IAAI,KAAK;AACX,WAAO,KAAK,KAAK;AACjB;;AAEF;;AAIF,MAAI,OAAO,KAAK;AACd,UAAO,KAAK,KAAK;AACjB;GACA,IAAI,QAAQ;AACZ,UAAO,IAAI,IACT,KAAI,KAAK,OAAO,QAAQ,IAAI,IAAI,KAAK;AACnC,WAAO,KAAK,KAAK;AACjB;AACA,WAAO,KAAK,KAAK;AACjB;cACS,KAAK,OAAO,OAAO,IAAI,IAAI,OAAO,KAAK,IAAI,OAAO,KAAK;AAChE,WAAO,KAAK,KAAK;AACjB;AACA,WAAO,KAAK,KAAK;AACjB;AACA;cACS,KAAK,OAAO,OAAO,QAAQ,GAAG;AACvC,WAAO,KAAK,KAAK;AACjB;AACA;cACS,KAAK,OAAO,OAAO,UAAU,GAAG;AACzC,WAAO,KAAK,KAAK;AACjB;AACA;UACK;AACL,WAAO,KAAK,KAAK;AACjB;;AAGJ;;AAIF,SAAO,KAAK;AACZ;;AAGF,QAAO,OAAO,KAAK,GAAG;;AAIxB,MAAM,SAAS;AACf,MAAM,eAAe;AACrB,MAAM,YAAY;AAClB,MAAM,YAAY;;;;;;;;;AAUlB,SAAgB,YAAY,MAAwB;CAClD,MAAM,WAAW,cAAc,KAAK;CACpC,MAAM,6BAAa,IAAI,KAAa;AAEpC,MAAK,MAAM,KAAK,SAAS,SAAS,OAAO,CAAE,YAAW,IAAI,EAAE,GAAG;AAC/D,MAAK,MAAM,KAAK,SAAS,SAAS,aAAa,CAAE,YAAW,IAAI,EAAE,GAAG;AACrE,MAAK,MAAM,KAAK,SAAS,SAAS,UAAU,CAAE,YAAW,IAAI,EAAE,GAAG;AAClE,MAAK,MAAM,KAAK,SAAS,SAAS,UAAU,CAAE,YAAW,IAAI,EAAE,GAAG;AAElE,QAAO,CAAC,GAAG,WAAW;;;;AC9HxB,MAAM,qBAAqB;CAAC;CAAO;CAAQ;CAAO;CAAQ;CAAQ;CAAO;AAEzE,SAAS,WAAW,UAA2B;CAC7C,MAAM,OAAO,SAAS,UAAU,EAAE,gBAAgB,OAAO,CAAC;AAC1D,QAAO,SAAS,KAAA,KAAa,KAAK,QAAQ;;AAG5C,SAAS,UAAU,UAA2B;CAC5C,MAAM,OAAO,SAAS,UAAU,EAAE,gBAAgB,OAAO,CAAC;AAC1D,QAAO,SAAS,KAAA,KAAa,KAAK,aAAa;;;;;;;AAQjD,SAAS,iBAAiB,WAA2B;AACnD,KAAI,UAAU,WAAW,IAAI,EAAE;EAC7B,MAAM,QAAQ,UAAU,MAAM,IAAI;AAClC,SAAO,MAAM,UAAU,IAAI,GAAG,MAAM,GAAG,GAAG,MAAM,OAAO;;AAEzD,QAAO,UAAU,MAAM,IAAI,CAAC;;AAG9B,SAAS,YACP,UACA,YACoB;AAEpB,KAAI,WAAW,SAAS,CAAE,QAAO;AAGjC,MAAK,MAAM,OAAO,YAAY;EAC5B,MAAM,UAAU,WAAW;AAC3B,MAAI,WAAW,QAAQ,CAAE,QAAO;;AAIlC,KAAI,UAAU,SAAS,CACrB,MAAK,MAAM,OAAO,YAAY;EAC5B,MAAM,YAAY,KAAK,UAAU,QAAQ,MAAM;AAC/C,MAAI,WAAW,UAAU,CAAE,QAAO;;;;;;;AAexC,SAAgB,eACd,SACA,SACU;CACV,MAAM,aAAa,QAAQ,cAAc;CAOzC,MAAM,uBAJU,QAAQ,UACpB,OAAO,QAAQ,QAAQ,QAAQ,CAAC,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAAE,GAAG,OAAO,GACzE,EAAE,EAE8B,KAAK,CAAC,KAAK,WAAW,CACxD,KACA,WAAW,MAAM,GAAG,QAAQ,QAAQ,SAAS,MAAM,CACpD,CAAU;CAEX,MAAM,wBAAQ,IAAI,KAAyC;AAE3D,QAAO,SAAS,iBACd,WACA,UAC4B;EAC5B,MAAM,UAAU,QAAQ,SAAS;EACjC,MAAM,WAAW,GAAG,UAAU,IAAI;AAElC,MAAI,MAAM,IAAI,SAAS,CAAE,QAAO,MAAM,IAAI,SAAS;EAEnD,IAAI;AAGJ,MAAI,UAAU,WAAW,KAAK,IAAI,UAAU,WAAW,MAAM,EAAE;GAC7D,MAAM,eAAe,YAAY,QAAQ,SAAS,UAAU,EAAE,WAAW;AACzE,OAAI,aACF,UAAS;IAAE,MAAM;IAAS;IAAc;SAIvC;GACH,IAAI,UAAU;AACd,QAAK,MAAM,CAAC,QAAQ,gBAAgB,oBAClC,KAAI,cAAc,UAAU,UAAU,WAAW,SAAS,IAAI,EAAE;IAE9D,MAAM,eAAe,YAAY,KAAK,aADzB,cAAc,SAAS,KAAK,UAAU,MAAM,OAAO,OAAO,CACf,EAAE,WAAW;AACrE,QAAI,aACF,UAAS;KAAE,MAAM;KAAS;KAAc;AAE1C,cAAU;AACV;;AAKJ,OAAI,CAAC,QACH,UAAS;IAAE,MAAM;IAAY,WAAW,iBAAiB,UAAU;IAAE;;AAIzE,QAAM,IAAI,UAAU,OAAO;AAC3B,SAAO;;;;;;;;;AC7GX,eAAsB,KACpB,WACA,SACqB;CACrB,MAAM,aAAa,QAAQ,UAAU;CAErC,MAAM,mBAAmB,eADT,QAAQ,UAAU,QAAQ,QAAQ,QAAQ,GAAG,QAAQ,KAAK,EACzB,QAAQ;CAEzD,MAAM,QAAkC,EAAE;CAC1C,MAAM,4BAAY,IAAI,KAAa;CACnC,MAAM,0BAAU,IAAI,KAAa;CAEjC,eAAe,MAAM,UAAiC;AACpD,MAAI,QAAQ,IAAI,SAAS,CAAE;AAC3B,UAAQ,IAAI,SAAS;EAGrB,MAAM,aAAa,YADH,MAAM,SAAS,UAAU,QAAQ,CACV;EAEvC,MAAM,YAAsB,EAAE;AAC9B,OAAK,MAAM,QAAQ,YAAY;GAC7B,MAAM,WAAW,iBAAiB,MAAM,SAAS;AACjD,OAAI,CAAC,SAAU;AAEf,OAAI,SAAS,SAAS,cAAc,SAAS,UAC3C,WAAU,IAAI,SAAS,UAAU;YACxB,SAAS,SAAS,WAAW,SAAS,aAC/C,WAAU,KAAK,SAAS,aAAa;;AAIzC,QAAM,YAAY;AAElB,QAAM,QAAQ,IAAI,UAAU,KAAK,QAAQ,MAAM,IAAI,CAAC,CAAC;;AAGvD,OAAM,MAAM,WAAW;CAGvB,MAAM,eAAyC,EAAE;AACjD,MAAK,MAAM,QAAQ,OAAO,KAAK,MAAM,CACnC,cAAa,QAAQ,EAAE;AAEzB,MAAK,MAAM,CAAC,MAAM,SAAS,OAAO,QAAQ,MAAM,CAC9C,MAAK,MAAM,OAAO,MAAM;AACtB,MAAI,CAAC,aAAa,KAAM,cAAa,OAAO,EAAE;AAC9C,eAAa,KAAK,KAAK,KAAK;;AAIhC,QAAO;EACL;EACA,OAAO,OAAO,KAAK,MAAM,CAAC,MAAM;EAChC,WAAW,CAAC,GAAG,UAAU,CAAC,MAAM;EAChC;EACA;EACD;;;;;;;;;;;;;;;;;;;;;AC3CH,eAAsB,UACpB,OACA,SACqB;AACrB,QAAO,KAAK,OAAO,WAAW,EAAE,CAAC;;;;;;;;;AAUnC,SAAgB,iBACd,MACA,aACU;CACV,MAAM,WAAW,QAAQ,YAAY;AAErC,KAAI,CAAC,KAAK,aAAa,UAAW,QAAO,EAAE;CAE3C,MAAM,2BAAW,IAAI,KAAa;CAClC,MAAM,QAAQ,CAAC,SAAS;AAExB,QAAO,MAAM,SAAS,GAAG;EACvB,MAAM,UAAU,MAAM,OAAO;EAC7B,MAAM,aAAa,KAAK,aAAa;AACrC,MAAI,CAAC,WAAY;AAEjB,OAAK,MAAM,UAAU,WACnB,KAAI,CAAC,SAAS,IAAI,OAAO,EAAE;AACzB,YAAS,IAAI,OAAO;AACpB,SAAM,KAAK,OAAO;;;AAKxB,QAAO,CAAC,GAAG,SAAS,CAAC,MAAM"}
|
|
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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "importree",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Build import dependency trees for TypeScript and JavaScript files. Fast, zero-dependency static analysis for dependency detection and cache invalidation.",
|
|
6
6
|
"author": "Alex Grozav <alex@grozav.com>",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/alexgrozav/importree.git"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://
|
|
12
|
+
"homepage": "https://importree.js.org",
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/alexgrozav/importree/issues"
|
|
15
15
|
},
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
],
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@vitest/coverage-v8": "^4.0.18",
|
|
49
|
+
"dependency-tree": "^11.4.0",
|
|
50
|
+
"madge": "^8.0.0",
|
|
49
51
|
"tsdown": "^0.21.0",
|
|
50
52
|
"typescript": "^5.7.0",
|
|
51
53
|
"vitest": "^4.0.18"
|
|
@@ -56,6 +58,8 @@
|
|
|
56
58
|
"build:watch": "tsdown --watch",
|
|
57
59
|
"dev": "tsdown --watch",
|
|
58
60
|
"dev:docs": "npx http-server docs -p 8765 -o",
|
|
61
|
+
"bench": "vitest bench",
|
|
62
|
+
"bench:run": "vitest bench --run",
|
|
59
63
|
"test": "vitest run",
|
|
60
64
|
"test:watch": "vitest"
|
|
61
65
|
}
|