Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ bunx foresthouse --help
- Honors the nearest `tsconfig.json` or `jsconfig.json`, including `baseUrl` and `paths`
- Expands sibling workspace packages by default, including their own `tsconfig` alias rules
- Analyzes React component and hook usage trees from explicit entry files or inferred Next.js app and pages routes
- Shows React tree changes relative to a Git ref or range with `foresthouse react --diff`
- Reads `package.json` manifests to show package-level dependency trees for single packages and monorepos
- Shows dependency-tree changes relative to a Git ref or range with `foresthouse deps --diff`
- Prints an ASCII tree by default, or JSON with `--json`
Expand Down Expand Up @@ -58,7 +59,7 @@ Options:
### Commands

- `foresthouse import <entry-file>`: analyzes a JavaScript or TypeScript entry file and prints a dependency tree by following imports, re-exports, `require()`, and dynamic `import()`.
- `foresthouse react [entry-file]`: analyzes React component, hook, and render relationships and prints a React usage tree. It also supports automatic Next.js entry discovery with `--nextjs`.
- `foresthouse react [entry-file]`: analyzes React component, hook, and render relationships and prints a React usage tree. It also supports automatic Next.js entry discovery with `--nextjs`, plus Git-based tree diffs with `--diff`.
- `foresthouse deps <directory>`: reads `package.json` manifests and workspace structure from a package directory and prints a package dependency tree. Use `--diff` to show only changes relative to a Git ref or range.

### Example
Expand Down Expand Up @@ -107,6 +108,7 @@ Usage:
$ foresthouse react [entry-file] [options]

Options:
--diff <git-ref-or-range> Show only React tree changes relative to a Git revision or range.
--cwd <path> Working directory used for relative paths.
--config <path> Explicit tsconfig.json or jsconfig.json path.
--nextjs Infer Next.js page entries from app/ and pages/ when no entry is provided.
Expand All @@ -118,6 +120,19 @@ Options:
-h, --help Display this message
```

React diff mode compares React usage graphs rather than raw file text diffs. A single ref like `--diff HEAD~3` compares that tree to the current working tree, while ranges such as `HEAD~3..HEAD` or `origin/dev...HEAD` compare two committed Git trees.

Example React diff output:

```text
~ apps/site/pages/index.tsx:21:45
~ <Home /> [component] (apps/site/pages/index.tsx)
└─ ~ <Hero /> [component] (apps/site/src/features/Main/Hero/Hero.tsx)
└─ + useForm() [hook] (react-hook-form)
```

In this example, the page entry itself is unchanged as source text, but its reachable React graph changed because `<Hero />` started calling `useForm()`.

### `foresthouse deps --help`

Analyzes package-level dependency trees for both single-package projects and monorepos. Use `--diff` to show only dependency changes relative to a Git ref or range.
Expand All @@ -137,6 +152,8 @@ Common examples:
- `foresthouse import src/main.ts`
- `foresthouse import --entry src/main.ts --cwd test/fixtures/basic`
- `foresthouse react src/App.tsx`
- `foresthouse react src/App.tsx --diff origin/dev...HEAD`
- `foresthouse react pages/index.tsx --cwd . --diff HEAD~3..HEAD`
- `foresthouse react --nextjs --cwd .`
- `foresthouse deps .`
- `foresthouse deps ./packages/a`
Expand Down Expand Up @@ -270,3 +287,4 @@ pnpm run check

- Unit tests live next to source files as `src/**/*.spec.ts`.
- End-to-end command tests live under `e2e/`.
- Benchmarks live next to analyzers as `src/**/*.bench.ts` and run with `pnpm exec vitest bench --run --config vitest.bench.config.ts`.
79 changes: 79 additions & 0 deletions bench/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'

const temporaryDirectories: string[] = []

let cleanupRegistered = false
const GIT_EXEC_MAX_BUFFER = 64 * 1024 * 1024

export function createGitRepository(
prefix: string,
commits: readonly {
readonly message: string
readonly files: Readonly<Record<string, string>>
}[],
): string {
registerCleanup()

const repositoryRoot = fs.mkdtempSync(path.join(os.tmpdir(), prefix))
temporaryDirectories.push(repositoryRoot)

runGit(repositoryRoot, ['init', '--initial-branch=main'])
runGit(repositoryRoot, ['config', 'user.name', 'Foresthouse Benchmarks'])
runGit(repositoryRoot, ['config', 'user.email', 'benchmarks@example.com'])

commits.forEach((commit) => {
replaceRepositoryFiles(repositoryRoot, commit.files)
runGit(repositoryRoot, ['add', '-A'])
runGit(repositoryRoot, ['commit', '-m', commit.message])
})

return repositoryRoot
}

function replaceRepositoryFiles(
repositoryRoot: string,
files: Readonly<Record<string, string>>,
): void {
fs.readdirSync(repositoryRoot, { withFileTypes: true }).forEach((entry) => {
if (entry.name === '.git') {
return
}

fs.rmSync(path.join(repositoryRoot, entry.name), {
recursive: true,
force: true,
})
})

Object.entries(files).forEach(([relativePath, fileContent]) => {
const absolutePath = path.join(repositoryRoot, relativePath)
fs.mkdirSync(path.dirname(absolutePath), { recursive: true })
fs.writeFileSync(absolutePath, fileContent)
})
}

function runGit(repositoryRoot: string, args: readonly string[]): string {
return execFileSync('git', args, {
cwd: repositoryRoot,
encoding: 'utf8',
maxBuffer: GIT_EXEC_MAX_BUFFER,
stdio: ['ignore', 'pipe', 'pipe'],
}).trim()
}

function registerCleanup(): void {
if (cleanupRegistered) {
return
}

cleanupRegistered = true

process.on('exit', () => {
temporaryDirectories.splice(0).forEach((directory) => {
fs.rmSync(directory, { recursive: true, force: true })
})
})
}
2 changes: 2 additions & 0 deletions e2e/deps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const pnpmMonorepoFixtureDirectory = path.join(
'deps-pnpm-monorepo',
)
const temporaryDirectories: string[] = []
const GIT_EXEC_MAX_BUFFER = 64 * 1024 * 1024

afterEach(() => {
temporaryDirectories.splice(0).forEach((directory) => {
Expand Down Expand Up @@ -1078,6 +1079,7 @@ function runGit(repositoryRoot: string, args: readonly string[]): string {
return execFileSync('git', args, {
cwd: repositoryRoot,
encoding: 'utf8',
maxBuffer: GIT_EXEC_MAX_BUFFER,
stdio: ['ignore', 'pipe', 'pipe'],
}).trim()
}
Loading
Loading