importree 2.0.0 → 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/dist/index.cjs +58 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +16 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +58 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
let node_path = require("node:path");
|
|
3
2
|
let node_fs = require("node:fs");
|
|
3
|
+
let node_path = require("node:path");
|
|
4
4
|
//#region src/scanner.ts
|
|
5
5
|
/**
|
|
6
6
|
* Strips comments from source code while preserving string literals.
|
|
@@ -209,6 +209,40 @@ function createResolver(basedir, options) {
|
|
|
209
209
|
}
|
|
210
210
|
//#endregion
|
|
211
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
|
+
}
|
|
212
246
|
/**
|
|
213
247
|
* Walks imports starting from an entry file and builds the full dependency tree.
|
|
214
248
|
* Uses iterative DFS with an explicit stack to avoid call-stack limits on deep chains.
|
|
@@ -224,34 +258,8 @@ async function walk(entryFile, options) {
|
|
|
224
258
|
const filePath = stack.pop();
|
|
225
259
|
if (visited.has(filePath)) continue;
|
|
226
260
|
visited.add(filePath);
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
for (const raw of rawImports) {
|
|
230
|
-
const resolved = resolveSpecifier(raw.path, filePath);
|
|
231
|
-
if (!resolved) continue;
|
|
232
|
-
if (resolved.type === "external" && resolved.specifier) externals.add(resolved.specifier);
|
|
233
|
-
else if (resolved.type === "local" && resolved.absolutePath) {
|
|
234
|
-
const existing = edges.find((e) => e.path === resolved.absolutePath);
|
|
235
|
-
if (existing) {
|
|
236
|
-
if (raw.isNamespace) {
|
|
237
|
-
existing.isNamespace = true;
|
|
238
|
-
existing.specifiers = void 0;
|
|
239
|
-
existing.isSideEffect = void 0;
|
|
240
|
-
} else if (raw.specifiers && !existing.isNamespace) {
|
|
241
|
-
(existing.specifiers ??= []).push(...raw.specifiers);
|
|
242
|
-
existing.isSideEffect = void 0;
|
|
243
|
-
}
|
|
244
|
-
if (raw.isSideEffect && !existing.specifiers && !existing.isNamespace) existing.isSideEffect = true;
|
|
245
|
-
if (raw.isDynamic) existing.isDynamic = true;
|
|
246
|
-
} else edges.push({
|
|
247
|
-
path: resolved.absolutePath,
|
|
248
|
-
specifiers: raw.specifiers,
|
|
249
|
-
isNamespace: raw.isNamespace,
|
|
250
|
-
isDynamic: raw.isDynamic,
|
|
251
|
-
isSideEffect: raw.isSideEffect
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
}
|
|
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);
|
|
255
263
|
graph[filePath] = edges;
|
|
256
264
|
for (const edge of edges) stack.push(edge.path);
|
|
257
265
|
}
|
|
@@ -298,6 +306,26 @@ async function importree(entry, options) {
|
|
|
298
306
|
return walk(entry, options ?? {});
|
|
299
307
|
}
|
|
300
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
|
+
/**
|
|
301
329
|
* Given an import tree and a changed file, returns all files that
|
|
302
330
|
* transitively depend on the changed file (i.e., files that would
|
|
303
331
|
* need to be re-evaluated if the changed file is modified).
|
|
@@ -324,5 +352,6 @@ function getAffectedFiles(tree, changedFile) {
|
|
|
324
352
|
//#endregion
|
|
325
353
|
exports.getAffectedFiles = getAffectedFiles;
|
|
326
354
|
exports.importree = importree;
|
|
355
|
+
exports.parseImports = parseImports;
|
|
327
356
|
|
|
328
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 * 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 { scanImports } from \"./scanner.js\";\nimport { createResolver } from \"./resolver.js\";\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\n const edges: ImportEdge[] = [];\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.add(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 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 { resolve } from \"node:path\";\nimport type { ImportreeOptions, ImportTree } from \"./types.js\";\nimport { walk } from \"./walker.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 * 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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"}
|
|
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
|
@@ -83,6 +83,21 @@ interface ImportTree {
|
|
|
83
83
|
*/
|
|
84
84
|
declare function importree(entry: string, options?: ImportreeOptions): Promise<ImportTree>;
|
|
85
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
|
+
/**
|
|
86
101
|
* Given an import tree and a changed file, returns all files that
|
|
87
102
|
* transitively depend on the changed file (i.e., files that would
|
|
88
103
|
* need to be re-evaluated if the changed file is modified).
|
|
@@ -91,5 +106,5 @@ declare function importree(entry: string, options?: ImportreeOptions): Promise<I
|
|
|
91
106
|
*/
|
|
92
107
|
declare function getAffectedFiles(tree: ImportTree, changedFile: string): string[];
|
|
93
108
|
//#endregion
|
|
94
|
-
export { type ImportEdge, type ImportTree, type ImportreeOptions, getAffectedFiles, importree };
|
|
109
|
+
export { type ImportEdge, type ImportTree, type ImportreeOptions, getAffectedFiles, importree, parseImports };
|
|
95
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;;;AAsBF;;;;EAdE,OAAA,GAAU,MAAA;;;;;;EAOV,UAAA;AAAA;;;;;UAOe,UAAA;;EAEf,IAAA;EAyCc;;;;EAnCd,UAAA;;EAGA,WAAA;;EAGA,SAAA;;EAGA,YAAA;AAAA;;;;UAMe,UAAA;
|
|
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
|
@@ -83,6 +83,21 @@ interface ImportTree {
|
|
|
83
83
|
*/
|
|
84
84
|
declare function importree(entry: string, options?: ImportreeOptions): Promise<ImportTree>;
|
|
85
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
|
+
/**
|
|
86
101
|
* Given an import tree and a changed file, returns all files that
|
|
87
102
|
* transitively depend on the changed file (i.e., files that would
|
|
88
103
|
* need to be re-evaluated if the changed file is modified).
|
|
@@ -91,5 +106,5 @@ declare function importree(entry: string, options?: ImportreeOptions): Promise<I
|
|
|
91
106
|
*/
|
|
92
107
|
declare function getAffectedFiles(tree: ImportTree, changedFile: string): string[];
|
|
93
108
|
//#endregion
|
|
94
|
-
export { type ImportEdge, type ImportTree, type ImportreeOptions, getAffectedFiles, importree };
|
|
109
|
+
export { type ImportEdge, type ImportTree, type ImportreeOptions, getAffectedFiles, importree, parseImports };
|
|
95
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;;;AAsBF;;;;EAdE,OAAA,GAAU,MAAA;;;;;;EAOV,UAAA;AAAA;;;;;UAOe,UAAA;;EAEf,IAAA;EAyCc;;;;EAnCd,UAAA;;EAGA,WAAA;;EAGA,SAAA;;EAGA,YAAA;AAAA;;;;UAMe,UAAA;
|
|
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,5 +1,5 @@
|
|
|
1
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
2
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
//#region src/scanner.ts
|
|
4
4
|
/**
|
|
5
5
|
* Strips comments from source code while preserving string literals.
|
|
@@ -208,6 +208,40 @@ function createResolver(basedir, options) {
|
|
|
208
208
|
}
|
|
209
209
|
//#endregion
|
|
210
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
|
+
}
|
|
211
245
|
/**
|
|
212
246
|
* Walks imports starting from an entry file and builds the full dependency tree.
|
|
213
247
|
* Uses iterative DFS with an explicit stack to avoid call-stack limits on deep chains.
|
|
@@ -223,34 +257,8 @@ async function walk(entryFile, options) {
|
|
|
223
257
|
const filePath = stack.pop();
|
|
224
258
|
if (visited.has(filePath)) continue;
|
|
225
259
|
visited.add(filePath);
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
for (const raw of rawImports) {
|
|
229
|
-
const resolved = resolveSpecifier(raw.path, filePath);
|
|
230
|
-
if (!resolved) continue;
|
|
231
|
-
if (resolved.type === "external" && resolved.specifier) externals.add(resolved.specifier);
|
|
232
|
-
else if (resolved.type === "local" && resolved.absolutePath) {
|
|
233
|
-
const existing = edges.find((e) => e.path === resolved.absolutePath);
|
|
234
|
-
if (existing) {
|
|
235
|
-
if (raw.isNamespace) {
|
|
236
|
-
existing.isNamespace = true;
|
|
237
|
-
existing.specifiers = void 0;
|
|
238
|
-
existing.isSideEffect = void 0;
|
|
239
|
-
} else if (raw.specifiers && !existing.isNamespace) {
|
|
240
|
-
(existing.specifiers ??= []).push(...raw.specifiers);
|
|
241
|
-
existing.isSideEffect = void 0;
|
|
242
|
-
}
|
|
243
|
-
if (raw.isSideEffect && !existing.specifiers && !existing.isNamespace) existing.isSideEffect = true;
|
|
244
|
-
if (raw.isDynamic) existing.isDynamic = true;
|
|
245
|
-
} else edges.push({
|
|
246
|
-
path: resolved.absolutePath,
|
|
247
|
-
specifiers: raw.specifiers,
|
|
248
|
-
isNamespace: raw.isNamespace,
|
|
249
|
-
isDynamic: raw.isDynamic,
|
|
250
|
-
isSideEffect: raw.isSideEffect
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
260
|
+
const { edges, externals: fileExternals } = buildEdges(scanImports(readFileSync(filePath, "utf-8")), resolveSpecifier, filePath);
|
|
261
|
+
for (const ext of fileExternals) externals.add(ext);
|
|
254
262
|
graph[filePath] = edges;
|
|
255
263
|
for (const edge of edges) stack.push(edge.path);
|
|
256
264
|
}
|
|
@@ -297,6 +305,26 @@ async function importree(entry, options) {
|
|
|
297
305
|
return walk(entry, options ?? {});
|
|
298
306
|
}
|
|
299
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
|
+
/**
|
|
300
328
|
* Given an import tree and a changed file, returns all files that
|
|
301
329
|
* transitively depend on the changed file (i.e., files that would
|
|
302
330
|
* need to be re-evaluated if the changed file is modified).
|
|
@@ -321,6 +349,6 @@ function getAffectedFiles(tree, changedFile) {
|
|
|
321
349
|
return [...affected].sort();
|
|
322
350
|
}
|
|
323
351
|
//#endregion
|
|
324
|
-
export { getAffectedFiles, importree };
|
|
352
|
+
export { getAffectedFiles, importree, parseImports };
|
|
325
353
|
|
|
326
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 * 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 { scanImports } from \"./scanner.js\";\nimport { createResolver } from \"./resolver.js\";\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\n const edges: ImportEdge[] = [];\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.add(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 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 { resolve } from \"node:path\";\nimport type { ImportreeOptions, ImportTree } from \"./types.js\";\nimport { walk } from \"./walker.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 * 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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"}
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "importree",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Build import dependency trees for TypeScript and JavaScript files. Fast, zero-dependency static analysis for dependency detection and cache invalidation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cache-invalidation",
|