foresthouse 1.1.0-dev.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/foresthouse.svg?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/foresthouse)
4
4
  [![codecov](https://img.shields.io/codecov/c/github/async3619/foresthouse?style=flat&colorA=000000&colorB=000000)](https://codecov.io/gh/async3619/foresthouse)
5
+ [![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json&style=flat&colorA=000000&colorB=000000)](https://codspeed.io/async3619/foresthouse?utm_source=badge)
5
6
  [![license](https://img.shields.io/npm/l/foresthouse?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/foresthouse)
6
7
 
7
8
  `foresthouse` is a modern TypeScript-first Node.js CLI that can print source import trees, React usage trees, and package-manifest dependency trees.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as printReactUsageTree, c as printPackageDependencyTree, f as analyzeReactUsage, g as analyzePackageDependencies, h as analyzePackageDependencyDiff, i as graphToSerializablePackageTree, m as isSourceCodeFile, n as graphToSerializableTree, o as printDependencyTree, p as analyzeDependencies, r as diffGraphToSerializablePackageTree, s as printPackageDependencyDiffTree, t as graphToSerializableReactTree } from "./react-GTkYqQLE.mjs";
2
+ import { a as printReactUsageTree, c as printPackageDependencyTree, f as analyzeReactUsage, g as analyzePackageDependencies, h as analyzePackageDependencyDiff, i as graphToSerializablePackageTree, m as isSourceCodeFile, n as graphToSerializableTree, o as printDependencyTree, p as analyzeDependencies, r as diffGraphToSerializablePackageTree, s as printPackageDependencyDiffTree, t as graphToSerializableReactTree } from "./react-xFA5HmqF.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as printReactUsageTree, c as printPackageDependencyTree, d as getReactUsageRoots, f as analyzeReactUsage, g as analyzePackageDependencies, h as analyzePackageDependencyDiff, i as graphToSerializablePackageTree, l as getFilteredUsages, n as graphToSerializableTree, o as printDependencyTree, p as analyzeDependencies, r as diffGraphToSerializablePackageTree, s as printPackageDependencyDiffTree, t as graphToSerializableReactTree, u as getReactUsageEntries } from "./react-GTkYqQLE.mjs";
1
+ import { a as printReactUsageTree, c as printPackageDependencyTree, d as getReactUsageRoots, f as analyzeReactUsage, g as analyzePackageDependencies, h as analyzePackageDependencyDiff, i as graphToSerializablePackageTree, l as getFilteredUsages, n as graphToSerializableTree, o as printDependencyTree, p as analyzeDependencies, r as diffGraphToSerializablePackageTree, s as printPackageDependencyDiffTree, t as graphToSerializableReactTree, u as getReactUsageEntries } from "./react-xFA5HmqF.mjs";
2
2
  export { analyzeDependencies, analyzePackageDependencies, analyzePackageDependencyDiff, analyzeReactUsage, diffGraphToSerializablePackageTree, getFilteredUsages, getReactUsageEntries, getReactUsageRoots, graphToSerializablePackageTree, graphToSerializableReactTree, graphToSerializableTree, printDependencyTree, printPackageDependencyDiffTree, printPackageDependencyTree, printReactUsageTree };
@@ -246,11 +246,11 @@ function listSubdirectories$1(directory) {
246
246
  }
247
247
  function createWorkspaceSegmentMatcher$1(segment) {
248
248
  if (!segment.includes("*")) return (name) => name === segment;
249
- const escapedSegment = escapeRegExp(segment).replaceAll("*", "[^/]*");
249
+ const escapedSegment = escapeRegExp$1(segment).replaceAll("*", "[^/]*");
250
250
  const pattern = new RegExp(`^${escapedSegment}$`);
251
251
  return (name) => pattern.test(name);
252
252
  }
253
- function escapeRegExp(value) {
253
+ function escapeRegExp$1(value) {
254
254
  return value.replaceAll(/[|\\{}()[\]^$+?.]/g, "\\$&");
255
255
  }
256
256
  function isRecord$1(value) {
@@ -760,20 +760,6 @@ function getCommandErrorMessage(error) {
760
760
  }
761
761
  //#endregion
762
762
  //#region src/typescript/program.ts
763
- function createProgram(entryFile, compilerOptions, currentDirectory) {
764
- const host = ts.createCompilerHost(compilerOptions, true);
765
- host.getCurrentDirectory = () => currentDirectory;
766
- if (ts.sys.realpath !== void 0) host.realpath = ts.sys.realpath;
767
- return ts.createProgram({
768
- rootNames: [entryFile],
769
- options: compilerOptions,
770
- host
771
- });
772
- }
773
- function createSourceFile(filePath) {
774
- const sourceText = fs.readFileSync(filePath, "utf8");
775
- return ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, getScriptKind(filePath));
776
- }
777
763
  function createModuleResolutionHost(currentDirectory) {
778
764
  return {
779
765
  fileExists: ts.sys.fileExists,
@@ -784,17 +770,6 @@ function createModuleResolutionHost(currentDirectory) {
784
770
  ...ts.sys.realpath === void 0 ? {} : { realpath: ts.sys.realpath }
785
771
  };
786
772
  }
787
- function getScriptKind(filePath) {
788
- switch (path.extname(filePath).toLowerCase()) {
789
- case ".js":
790
- case ".mjs":
791
- case ".cjs": return ts.ScriptKind.JS;
792
- case ".jsx": return ts.ScriptKind.JSX;
793
- case ".tsx": return ts.ScriptKind.TSX;
794
- case ".json": return ts.ScriptKind.JSON;
795
- default: return ts.ScriptKind.TS;
796
- }
797
- }
798
773
  //#endregion
799
774
  //#region src/typescript/config.ts
800
775
  const nearestConfigCache = /* @__PURE__ */ new Map();
@@ -1142,79 +1117,11 @@ function resolveExistingPath(cwd, entryFile) {
1142
1117
  return normalizedPath;
1143
1118
  }
1144
1119
  //#endregion
1145
- //#region src/analyzers/import/unused.ts
1146
- function collectUnusedImports(sourceFile, checker) {
1147
- const importUsage = /* @__PURE__ */ new Map();
1148
- const symbolToImportDeclaration = /* @__PURE__ */ new Map();
1149
- const importedLocalNames = /* @__PURE__ */ new Set();
1150
- sourceFile.statements.forEach((statement) => {
1151
- if (!ts.isImportDeclaration(statement) || statement.importClause === void 0) return;
1152
- const identifiers = getImportBindingIdentifiers(statement.importClause);
1153
- if (identifiers.length === 0) return;
1154
- importUsage.set(statement, {
1155
- canTrack: false,
1156
- used: false
1157
- });
1158
- identifiers.forEach((identifier) => {
1159
- importedLocalNames.add(identifier.text);
1160
- const symbol = tryGetSymbolAtLocation(checker, identifier);
1161
- if (symbol === void 0) return;
1162
- symbolToImportDeclaration.set(symbol, statement);
1163
- const state = importUsage.get(statement);
1164
- if (state !== void 0) state.canTrack = true;
1165
- });
1166
- });
1167
- function visit(node) {
1168
- if (ts.isImportDeclaration(node)) return;
1169
- if (ts.isIdentifier(node) && importedLocalNames.has(node.text) && isReferenceIdentifier(node)) {
1170
- const symbol = tryGetSymbolAtLocation(checker, node);
1171
- const declaration = symbol === void 0 ? void 0 : symbolToImportDeclaration.get(symbol);
1172
- if (declaration !== void 0) {
1173
- const state = importUsage.get(declaration);
1174
- if (state !== void 0) state.used = true;
1175
- }
1176
- }
1177
- ts.forEachChild(node, visit);
1178
- }
1179
- visit(sourceFile);
1180
- return new Map([...importUsage.entries()].map(([declaration, state]) => [declaration, state.canTrack && !state.used]));
1181
- }
1182
- function getImportBindingIdentifiers(importClause) {
1183
- const identifiers = [];
1184
- if (importClause.name !== void 0) identifiers.push(importClause.name);
1185
- const namedBindings = importClause.namedBindings;
1186
- if (namedBindings === void 0) return identifiers;
1187
- if (ts.isNamespaceImport(namedBindings)) {
1188
- identifiers.push(namedBindings.name);
1189
- return identifiers;
1190
- }
1191
- namedBindings.elements.forEach((element) => {
1192
- identifiers.push(element.name);
1193
- });
1194
- return identifiers;
1195
- }
1196
- function isReferenceIdentifier(node) {
1197
- const parent = node.parent;
1198
- if (ts.isPropertyAccessExpression(parent) && parent.name === node) return false;
1199
- if (ts.isQualifiedName(parent) && parent.right === node) return false;
1200
- if (ts.isPropertyAssignment(parent) && parent.name === node) return false;
1201
- if (ts.isBindingElement(parent) && parent.propertyName === node) return false;
1202
- if (ts.isJsxAttribute(parent) && parent.name === node) return false;
1203
- if (ts.isExportSpecifier(parent)) return parent.propertyName === node || parent.propertyName === void 0;
1204
- return true;
1205
- }
1206
- function tryGetSymbolAtLocation(checker, node) {
1207
- try {
1208
- return checker.getSymbolAtLocation(node);
1209
- } catch {
1210
- return;
1211
- }
1212
- }
1213
- //#endregion
1214
1120
  //#region src/analyzers/import/references.ts
1215
- function collectModuleReferences(sourceFile, checker) {
1121
+ function collectModuleReferences(filePath, sourceText, trackUnusedImports) {
1122
+ const parseResult = parseSync(filePath, sourceText);
1123
+ const mod = parseResult.module;
1216
1124
  const references = /* @__PURE__ */ new Map();
1217
- const unusedImports = checker === void 0 ? /* @__PURE__ */ new Map() : collectUnusedImports(sourceFile, checker);
1218
1125
  function addReference(specifier, referenceKind, isTypeOnly, unused) {
1219
1126
  const key = `${referenceKind}:${isTypeOnly ? "type" : "value"}:${specifier}`;
1220
1127
  const existing = references.get(key);
@@ -1232,26 +1139,76 @@ function collectModuleReferences(sourceFile, checker) {
1232
1139
  unused
1233
1140
  });
1234
1141
  }
1142
+ for (const imp of mod.staticImports) {
1143
+ const specifier = imp.moduleRequest.value;
1144
+ const isTypeOnly = imp.entries.length > 0 && imp.entries.every((e) => e.isType);
1145
+ let unused = false;
1146
+ if (trackUnusedImports && imp.entries.length > 0) {
1147
+ const localNames = imp.entries.map((e) => e.localName.value);
1148
+ unused = !isAnyNameUsedAfter(imp.end, localNames, sourceText);
1149
+ }
1150
+ addReference(specifier, "import", isTypeOnly, unused);
1151
+ }
1152
+ for (const exp of mod.staticExports) {
1153
+ const reExportEntry = exp.entries.find((e) => e.moduleRequest !== null);
1154
+ if (reExportEntry?.moduleRequest != null) {
1155
+ const specifier = reExportEntry.moduleRequest.value;
1156
+ addReference(specifier, "export", exp.entries.every((e) => e.isType), false);
1157
+ }
1158
+ }
1159
+ for (const di of mod.dynamicImports) {
1160
+ const req = di.moduleRequest;
1161
+ if (req.start != null && req.end != null) {
1162
+ const specifier = extractStringLiteral(sourceText, req.start, req.end);
1163
+ if (specifier !== void 0) addReference(specifier, "dynamic-import", false, false);
1164
+ }
1165
+ }
1166
+ if (sourceText.includes("require(")) collectRequireReferences(parseResult.program.body, addReference);
1167
+ return [...references.values()];
1168
+ }
1169
+ function isAnyNameUsedAfter(afterPosition, names, sourceText) {
1170
+ const restOfFile = sourceText.slice(afterPosition);
1171
+ return names.some((name) => {
1172
+ return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(restOfFile);
1173
+ });
1174
+ }
1175
+ function escapeRegExp(string) {
1176
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1177
+ }
1178
+ function extractStringLiteral(sourceText, start, end) {
1179
+ const raw = sourceText.slice(start, end);
1180
+ if (raw.startsWith("'") && raw.endsWith("'") || raw.startsWith("\"") && raw.endsWith("\"") || raw.startsWith("`") && raw.endsWith("`") && !raw.includes("${")) return raw.slice(1, -1);
1181
+ }
1182
+ function collectRequireReferences(body, addReference) {
1235
1183
  function visit(node) {
1236
- if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) addReference(node.moduleSpecifier.text, "import", node.importClause?.isTypeOnly ?? false, unusedImports.get(node) ?? false);
1237
- else if (ts.isExportDeclaration(node) && node.moduleSpecifier !== void 0 && ts.isStringLiteralLike(node.moduleSpecifier)) addReference(node.moduleSpecifier.text, "export", node.isTypeOnly ?? false, false);
1238
- else if (ts.isImportEqualsDeclaration(node)) {
1239
- const moduleReference = node.moduleReference;
1240
- if (ts.isExternalModuleReference(moduleReference) && moduleReference.expression !== void 0 && ts.isStringLiteralLike(moduleReference.expression)) addReference(moduleReference.expression.text, "import-equals", false, false);
1241
- } else if (ts.isCallExpression(node)) {
1242
- if (node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length === 1) {
1243
- const [argument] = node.arguments;
1244
- if (argument !== void 0 && ts.isStringLiteralLike(argument)) addReference(argument.text, "dynamic-import", false, false);
1184
+ if (node === null || node === void 0 || typeof node !== "object") return;
1185
+ const n = node;
1186
+ if (n.type === "TSImportEqualsDeclaration") {
1187
+ const moduleRef = n.moduleReference;
1188
+ if (moduleRef?.type === "TSExternalModuleReference") {
1189
+ const expr = moduleRef.expression;
1190
+ if (expr?.type === "Literal" && typeof expr.value === "string") {
1191
+ addReference(expr.value, "import-equals", false, false);
1192
+ return;
1193
+ }
1245
1194
  }
1246
- if (ts.isIdentifier(node.expression) && node.expression.text === "require" && node.arguments.length === 1) {
1247
- const [argument] = node.arguments;
1248
- if (argument !== void 0 && ts.isStringLiteralLike(argument)) addReference(argument.text, "require", false, false);
1195
+ }
1196
+ if (n.type === "CallExpression") {
1197
+ const callee = n.callee;
1198
+ const args = n.arguments;
1199
+ if (callee?.type === "Identifier" && callee.name === "require" && Array.isArray(args) && args.length === 1) {
1200
+ const arg = args[0];
1201
+ if (arg?.type === "Literal" && typeof arg.value === "string") {
1202
+ addReference(arg.value, "require", false, false);
1203
+ return;
1204
+ }
1249
1205
  }
1250
1206
  }
1251
- ts.forEachChild(node, visit);
1207
+ for (const value of Object.values(n)) if (Array.isArray(value)) {
1208
+ for (const item of value) if (item && typeof item === "object" && "type" in item) visit(item);
1209
+ } else if (value && typeof value === "object" && "type" in value) visit(value);
1252
1210
  }
1253
- visit(sourceFile);
1254
- return [...references.values()];
1211
+ for (const stmt of body) visit(stmt);
1255
1212
  }
1256
1213
  //#endregion
1257
1214
  //#region src/analyzers/import/resolver.ts
@@ -1351,8 +1308,6 @@ function buildDependencyGraph(entryConfigs, options) {
1351
1308
  var DependencyGraphBuilder = class {
1352
1309
  nodes = /* @__PURE__ */ new Map();
1353
1310
  configCache = /* @__PURE__ */ new Map();
1354
- programCache = /* @__PURE__ */ new Map();
1355
- checkerCache = /* @__PURE__ */ new Map();
1356
1311
  resolverCache = /* @__PURE__ */ new Map();
1357
1312
  resolutionCache = /* @__PURE__ */ new Map();
1358
1313
  packageRootCache = /* @__PURE__ */ new Map();
@@ -1376,18 +1331,13 @@ var DependencyGraphBuilder = class {
1376
1331
  const normalizedPath = normalizeFilePath(filePath);
1377
1332
  if (this.nodes.has(normalizedPath)) return;
1378
1333
  const config = this.getConfigForFile(normalizedPath);
1379
- const checker = this.options.trackUnusedImports ? this.getCheckerForFile(normalizedPath, config) : void 0;
1380
- const dependencies = collectModuleReferences(this.getSourceFileForFile(normalizedPath, config), checker).map((reference) => this.resolveDependencyWithCache(reference, normalizedPath, config, entryConfigPath));
1334
+ const dependencies = collectModuleReferences(normalizedPath, fs.readFileSync(normalizedPath, "utf8"), this.options.trackUnusedImports).map((reference) => this.resolveDependencyWithCache(reference, normalizedPath, config, entryConfigPath));
1381
1335
  this.nodes.set(normalizedPath, {
1382
1336
  id: normalizedPath,
1383
1337
  dependencies
1384
1338
  });
1385
1339
  for (const dependency of dependencies) if (dependency.kind === "source") this.visitFile(dependency.target, entryConfigPath);
1386
1340
  }
1387
- getSourceFileForFile(filePath, config) {
1388
- if (!this.options.trackUnusedImports) return createSourceFile(filePath);
1389
- return this.getProgramForFile(filePath, config).getSourceFile(filePath) ?? createSourceFile(filePath);
1390
- }
1391
1341
  getConfigForFile(filePath) {
1392
1342
  const directory = path.dirname(filePath);
1393
1343
  const cached = this.configCache.get(directory);
@@ -1396,29 +1346,12 @@ var DependencyGraphBuilder = class {
1396
1346
  this.configCache.set(directory, loaded);
1397
1347
  return loaded;
1398
1348
  }
1399
- getProgramForFile(filePath, config) {
1400
- const cacheKey = this.getProgramCacheKey(filePath, config);
1401
- const cached = this.programCache.get(cacheKey);
1402
- if (cached !== void 0) return cached;
1403
- const currentDirectory = config.path === void 0 ? path.dirname(filePath) : path.dirname(config.path);
1404
- const program = createProgram(filePath, config.compilerOptions, currentDirectory);
1405
- this.programCache.set(cacheKey, program);
1406
- return program;
1407
- }
1408
- getCheckerForFile(filePath, config) {
1409
- const cacheKey = this.getProgramCacheKey(filePath, config);
1410
- const cached = this.checkerCache.get(cacheKey);
1411
- if (cached !== void 0) return cached;
1412
- const checker = this.getProgramForFile(filePath, config).getTypeChecker();
1413
- this.checkerCache.set(cacheKey, checker);
1414
- return checker;
1415
- }
1416
- getProgramCacheKey(filePath, config) {
1349
+ getConfigCacheKey(filePath, config) {
1417
1350
  return config.path ?? `default:${path.dirname(filePath)}`;
1418
1351
  }
1419
1352
  getResolverForFile(filePath) {
1420
1353
  const config = this.getConfigForFile(filePath);
1421
- const cacheKey = this.getProgramCacheKey(filePath, config);
1354
+ const cacheKey = this.getConfigCacheKey(filePath, config);
1422
1355
  const cached = this.resolverCache.get(cacheKey);
1423
1356
  if (cached !== void 0) return cached;
1424
1357
  const resolver = new ResolverFactory({
@@ -1472,7 +1405,7 @@ var DependencyGraphBuilder = class {
1472
1405
  const scopeKey = entryConfigPath ?? "";
1473
1406
  if (isPackageLikeImport(specifier)) {
1474
1407
  const packageRoot = this.getPackageRoot(containingFile) ?? path.dirname(containingFile);
1475
- return `package:${this.getProgramCacheKey(containingFile, config)}:${packageRoot}:${specifier}:${scopeKey}`;
1408
+ return `package:${this.getConfigCacheKey(containingFile, config)}:${packageRoot}:${specifier}:${scopeKey}`;
1476
1409
  }
1477
1410
  return `path:${path.dirname(containingFile)}:${specifier}:${scopeKey}`;
1478
1411
  }
@@ -2892,4 +2825,4 @@ function formatReactNodeFilePath(filePath, kind, cwd) {
2892
2825
  //#endregion
2893
2826
  export { printReactUsageTree as a, printPackageDependencyTree as c, getReactUsageRoots as d, analyzeReactUsage as f, analyzePackageDependencies as g, analyzePackageDependencyDiff as h, graphToSerializablePackageTree as i, getFilteredUsages as l, isSourceCodeFile as m, graphToSerializableTree as n, printDependencyTree as o, analyzeDependencies as p, diffGraphToSerializablePackageTree as r, printPackageDependencyDiffTree as s, graphToSerializableReactTree as t, getReactUsageEntries as u };
2894
2827
 
2895
- //# sourceMappingURL=react-GTkYqQLE.mjs.map
2828
+ //# sourceMappingURL=react-xFA5HmqF.mjs.map