foresthouse 1.0.0-dev.2 → 1.0.0-dev.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -3
- package/dist/cli.mjs +272 -110
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +180 -23
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/react-DWxB8qZ6.mjs +2484 -0
- package/dist/react-DWxB8qZ6.mjs.map +1 -0
- package/package.json +3 -1
- package/dist/tree-Cp8CNmeY.mjs +0 -755
- package/dist/tree-Cp8CNmeY.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# foresthouse
|
|
2
2
|
|
|
3
|
-
`foresthouse` is a modern TypeScript-first Node.js CLI that
|
|
3
|
+
`foresthouse` is a modern TypeScript-first Node.js CLI that can print source import trees, React usage trees, and package-manifest dependency trees.
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
- Reads a JavaScript/TypeScript entry file (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.mts`, `.cts`)
|
|
19
19
|
- Resolves local imports, re-exports, `require()`, and string-literal dynamic `import()`
|
|
20
20
|
- Honors the nearest `tsconfig.json` or `jsconfig.json`, including `baseUrl` and `paths`
|
|
21
|
+
- Expands sibling workspace packages by default, including their own `tsconfig` alias rules
|
|
22
|
+
- Reads `package.json` manifests to show package-level dependency trees for single packages and monorepos
|
|
21
23
|
- Prints a tree by default, or JSON with `--json`
|
|
24
|
+
- Colorizes ASCII output automatically when the terminal supports ANSI colors
|
|
22
25
|
|
|
23
26
|
## Usage
|
|
24
27
|
|
|
@@ -26,13 +29,14 @@
|
|
|
26
29
|
corepack enable
|
|
27
30
|
pnpm install
|
|
28
31
|
pnpm run build
|
|
29
|
-
node dist/cli.mjs src/index.ts
|
|
32
|
+
node dist/cli.mjs import src/index.ts
|
|
33
|
+
node dist/cli.mjs deps .
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
### Example
|
|
33
37
|
|
|
34
38
|
```bash
|
|
35
|
-
node dist/cli.mjs src/main.ts --cwd test/fixtures/basic
|
|
39
|
+
node dist/cli.mjs import src/main.ts --cwd test/fixtures/basic
|
|
36
40
|
```
|
|
37
41
|
|
|
38
42
|
Output:
|
|
@@ -48,11 +52,38 @@ src/main.ts
|
|
|
48
52
|
|
|
49
53
|
### Options
|
|
50
54
|
|
|
55
|
+
`import` command:
|
|
56
|
+
|
|
57
|
+
- `foresthouse import <path>`: analyze an entry file by positional argument
|
|
58
|
+
- `foresthouse import --entry <path>`: analyze an entry file by explicit option
|
|
51
59
|
- `--cwd <path>`: working directory for resolving the entry file and config
|
|
52
60
|
- `--config <path>`: use a specific `tsconfig.json` or `jsconfig.json`
|
|
53
61
|
- `--include-externals`: include packages and Node built-ins in the output
|
|
62
|
+
- `--no-workspaces`: stop at sibling workspace package boundaries instead of expanding them
|
|
63
|
+
- `--project-only`: restrict traversal to the active `tsconfig.json` or `jsconfig.json` project
|
|
64
|
+
- `--no-unused`: omit imports that are never referenced
|
|
65
|
+
- `--json`: print a JSON tree instead of ASCII output
|
|
66
|
+
|
|
67
|
+
`react` command:
|
|
68
|
+
|
|
69
|
+
- `foresthouse react <path>`: print a React usage tree from an entry file
|
|
70
|
+
- `foresthouse react --nextjs`: infer Next.js page entries from `app/`, `pages/`, `src/app/`, and `src/pages/`
|
|
71
|
+
- `--cwd <path>`: working directory for resolving the entry file and config
|
|
72
|
+
- `--config <path>`: use a specific `tsconfig.json` or `jsconfig.json`
|
|
73
|
+
- `--nextjs`: allow omitting the explicit React entry file and discover Next.js page entries relative to `--cwd` or the current directory
|
|
74
|
+
- `--filter <component|hook|builtin>`: limit the output to a specific React symbol kind
|
|
75
|
+
- `--builtin`: include built-in HTML nodes such as `button` and `div`
|
|
76
|
+
- `--no-workspaces`: stop at sibling workspace package boundaries instead of expanding them
|
|
77
|
+
- `--project-only`: restrict traversal to the active `tsconfig.json` or `jsconfig.json` project
|
|
54
78
|
- `--json`: print a JSON tree instead of ASCII output
|
|
55
79
|
|
|
80
|
+
`deps` command:
|
|
81
|
+
|
|
82
|
+
- `foresthouse deps <directory>`: analyze the nearest package rooted at the given directory
|
|
83
|
+
- `foresthouse deps .`: analyze the current package or repository root
|
|
84
|
+
- `foresthouse deps ./packages/a`: analyze a specific workspace package directory
|
|
85
|
+
- `--json`: print a JSON package tree instead of ASCII output
|
|
86
|
+
|
|
56
87
|
## Development
|
|
57
88
|
|
|
58
89
|
```bash
|
package/dist/cli.mjs
CHANGED
|
@@ -1,129 +1,291 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as
|
|
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-DWxB8qZ6.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
process.stdout.write(`${printReactUsageTree(graph, {
|
|
20
|
-
cwd: options.cwd ?? graph.cwd,
|
|
21
|
-
filter: options.react
|
|
22
|
-
})}\n`);
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process$1 from "node:process";
|
|
7
|
+
import { cac } from "cac";
|
|
8
|
+
//#region src/commands/base.ts
|
|
9
|
+
var BaseCommand = class {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
run() {
|
|
14
|
+
const graph = this.analyze();
|
|
15
|
+
if (this.isJsonMode()) {
|
|
16
|
+
process.stdout.write(`${JSON.stringify(this.serialize(graph), null, 2)}\n`);
|
|
23
17
|
return;
|
|
24
18
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
process.stdout.write(`${this.render(graph)}\n`);
|
|
20
|
+
}
|
|
21
|
+
isJsonMode() {
|
|
22
|
+
return this.options.json;
|
|
23
|
+
}
|
|
24
|
+
getAnalyzeOptions() {
|
|
25
|
+
return {
|
|
26
|
+
...this.options.cwd === void 0 ? {} : { cwd: this.options.cwd },
|
|
27
|
+
...this.options.configPath === void 0 ? {} : { configPath: this.options.configPath },
|
|
28
|
+
expandWorkspaces: this.options.expandWorkspaces,
|
|
29
|
+
projectOnly: this.options.projectOnly
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/commands/deps.ts
|
|
35
|
+
var DepsCommand = class extends BaseCommand {
|
|
36
|
+
analyze() {
|
|
37
|
+
if (this.options.diff !== void 0) return analyzePackageDependencyDiff(this.options.directory, this.options.diff);
|
|
38
|
+
return analyzePackageDependencies(this.options.directory);
|
|
39
|
+
}
|
|
40
|
+
serialize(graph) {
|
|
41
|
+
if (isPackageDependencyDiffGraph(graph)) return diffGraphToSerializablePackageTree(graph);
|
|
42
|
+
return graphToSerializablePackageTree(graph);
|
|
43
|
+
}
|
|
44
|
+
render(graph) {
|
|
45
|
+
if (isPackageDependencyDiffGraph(graph)) return printPackageDependencyDiffTree(graph);
|
|
46
|
+
return printPackageDependencyTree(graph);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function isPackageDependencyDiffGraph(graph) {
|
|
50
|
+
return "root" in graph;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/commands/import.ts
|
|
54
|
+
var ImportCommand = class extends BaseCommand {
|
|
55
|
+
analyze() {
|
|
56
|
+
return analyzeDependencies(this.options.entryFile, this.getAnalyzeOptions());
|
|
57
|
+
}
|
|
58
|
+
serialize(graph) {
|
|
59
|
+
return graphToSerializableTree(graph, { omitUnused: this.options.omitUnused });
|
|
60
|
+
}
|
|
61
|
+
render(graph) {
|
|
62
|
+
return printDependencyTree(graph, {
|
|
63
|
+
cwd: this.options.cwd ?? graph.cwd,
|
|
64
|
+
includeExternals: this.options.includeExternals,
|
|
65
|
+
omitUnused: this.options.omitUnused
|
|
28
66
|
});
|
|
29
|
-
if (options.json) {
|
|
30
|
-
process.stdout.write(`${JSON.stringify(graphToSerializableTree(graph), null, 2)}\n`);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
process.stdout.write(`${printDependencyTree(graph, {
|
|
34
|
-
cwd: options.cwd ?? graph.cwd,
|
|
35
|
-
includeExternals: options.includeExternals
|
|
36
|
-
})}\n`);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
const message = error instanceof Error ? error.message : "Unknown error occurred.";
|
|
39
|
-
process.stderr.write(`foresthouse: ${message}\n`);
|
|
40
|
-
process.exitCode = 1;
|
|
41
67
|
}
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/app/react-entry-files.ts
|
|
71
|
+
const NEXT_JS_ENTRY_ROOTS = [
|
|
72
|
+
"pages",
|
|
73
|
+
"app",
|
|
74
|
+
path.join("src", "pages"),
|
|
75
|
+
path.join("src", "app")
|
|
76
|
+
];
|
|
77
|
+
function resolveReactEntryFiles(options) {
|
|
78
|
+
if (options.entryFile !== void 0) return [options.entryFile];
|
|
79
|
+
if (!options.nextjs) throw new Error("Missing React entry file. Use `foresthouse react <entry-file>` or `foresthouse react --nextjs`.");
|
|
80
|
+
return discoverNextJsPageEntries(options.cwd);
|
|
42
81
|
}
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const value = argument.slice(8);
|
|
85
|
-
if (value === "component" || value === "hook") {
|
|
86
|
-
react = value;
|
|
82
|
+
function discoverNextJsPageEntries(cwd) {
|
|
83
|
+
const effectiveCwd = path.resolve(cwd ?? process.cwd());
|
|
84
|
+
const entries = /* @__PURE__ */ new Set();
|
|
85
|
+
collectPagesRouterEntries(path.join(effectiveCwd, "pages"), effectiveCwd, entries);
|
|
86
|
+
collectAppRouterEntries(path.join(effectiveCwd, "app"), effectiveCwd, entries);
|
|
87
|
+
collectPagesRouterEntries(path.join(effectiveCwd, "src", "pages"), effectiveCwd, entries);
|
|
88
|
+
collectAppRouterEntries(path.join(effectiveCwd, "src", "app"), effectiveCwd, entries);
|
|
89
|
+
const resolvedEntries = [...entries].sort();
|
|
90
|
+
if (resolvedEntries.length > 0) return resolvedEntries;
|
|
91
|
+
const searchedRoots = NEXT_JS_ENTRY_ROOTS.map((root) => `\`${root}/\``).join(", ");
|
|
92
|
+
throw new Error(`No Next.js page entries found. Searched ${searchedRoots} relative to ${effectiveCwd}.`);
|
|
93
|
+
}
|
|
94
|
+
function collectPagesRouterEntries(rootDirectory, cwd, entries) {
|
|
95
|
+
walkDirectory(rootDirectory, (filePath, relativePath) => {
|
|
96
|
+
if (!isSourceCodeFile(filePath) || filePath.endsWith(".d.ts")) return;
|
|
97
|
+
if (relativePath.split(path.sep)[0] === "api") return;
|
|
98
|
+
if (path.basename(filePath).startsWith("_")) return;
|
|
99
|
+
entries.add(path.relative(cwd, filePath));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function collectAppRouterEntries(rootDirectory, cwd, entries) {
|
|
103
|
+
walkDirectory(rootDirectory, (filePath) => {
|
|
104
|
+
if (!isSourceCodeFile(filePath) || filePath.endsWith(".d.ts")) return;
|
|
105
|
+
const extension = path.extname(filePath);
|
|
106
|
+
if (path.basename(filePath, extension) !== "page") return;
|
|
107
|
+
entries.add(path.relative(cwd, filePath));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function walkDirectory(rootDirectory, visitor) {
|
|
111
|
+
if (!fs.existsSync(rootDirectory)) return;
|
|
112
|
+
const stack = [rootDirectory];
|
|
113
|
+
while (stack.length > 0) {
|
|
114
|
+
const directory = stack.pop();
|
|
115
|
+
if (directory === void 0) continue;
|
|
116
|
+
const directoryEntries = fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
117
|
+
for (let index = directoryEntries.length - 1; index >= 0; index -= 1) {
|
|
118
|
+
const entry = directoryEntries[index];
|
|
119
|
+
if (entry === void 0) continue;
|
|
120
|
+
const entryPath = path.join(directory, entry.name);
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
stack.push(entryPath);
|
|
87
123
|
continue;
|
|
88
124
|
}
|
|
89
|
-
|
|
125
|
+
if (!entry.isFile()) continue;
|
|
126
|
+
visitor(entryPath, path.relative(rootDirectory, entryPath));
|
|
90
127
|
}
|
|
91
|
-
if (argument.startsWith("-")) throw new Error(`Unknown option: ${argument}`);
|
|
92
|
-
if (entryFile !== void 0) throw new Error("Only one entry file can be provided.");
|
|
93
|
-
entryFile = argument;
|
|
94
128
|
}
|
|
95
|
-
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/commands/react.ts
|
|
132
|
+
var ReactCommand = class extends BaseCommand {
|
|
133
|
+
analyze() {
|
|
134
|
+
return analyzeReactUsage(resolveReactEntryFiles(this.options), this.getAnalyzeOptions());
|
|
135
|
+
}
|
|
136
|
+
serialize(graph) {
|
|
137
|
+
return graphToSerializableReactTree(graph, { filter: this.getFilter() });
|
|
138
|
+
}
|
|
139
|
+
render(graph) {
|
|
140
|
+
return printReactUsageTree(graph, {
|
|
141
|
+
cwd: this.options.cwd ?? graph.cwd,
|
|
142
|
+
filter: this.getFilter()
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
getAnalyzeOptions() {
|
|
146
|
+
return {
|
|
147
|
+
...super.getAnalyzeOptions(),
|
|
148
|
+
includeBuiltins: this.options.includeBuiltins
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
getFilter() {
|
|
152
|
+
return this.options.filter;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/app/run.ts
|
|
157
|
+
function runCli(options) {
|
|
158
|
+
new CliApplication(options).run();
|
|
159
|
+
}
|
|
160
|
+
var CliApplication = class {
|
|
161
|
+
constructor(options) {
|
|
162
|
+
this.options = options;
|
|
163
|
+
}
|
|
164
|
+
run() {
|
|
165
|
+
this.createCommand().run();
|
|
166
|
+
}
|
|
167
|
+
createCommand() {
|
|
168
|
+
if (this.options.command === "deps") return new DepsCommand(this.options);
|
|
169
|
+
if (this.options.command === "react") return new ReactCommand(this.options);
|
|
170
|
+
return new ImportCommand(this.options);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/app/cli.ts
|
|
175
|
+
function main(version, argv = process$1.argv.slice(2)) {
|
|
176
|
+
new CliMain(version, argv).run();
|
|
177
|
+
}
|
|
178
|
+
var CliMain = class {
|
|
179
|
+
constructor(version, argv) {
|
|
180
|
+
this.version = version;
|
|
181
|
+
this.argv = argv;
|
|
182
|
+
}
|
|
183
|
+
run() {
|
|
184
|
+
try {
|
|
185
|
+
const cli = this.createCli();
|
|
186
|
+
if (this.argv.length === 0) {
|
|
187
|
+
cli.outputHelp();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.validateCommandSyntax();
|
|
191
|
+
cli.parse(this.toProcessArgv());
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred.";
|
|
194
|
+
process$1.stderr.write(`foresthouse: ${message}\n`);
|
|
195
|
+
process$1.exitCode = 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
createCli() {
|
|
199
|
+
const cli = cac("foresthouse");
|
|
200
|
+
cli.command("deps <directory>", "Analyze package.json dependencies from a package directory.").usage("deps <directory> [options]").option("--diff <git-ref-or-range>", "Show only dependency-tree changes relative to a Git revision or range.").option("--json", "Print the package tree as JSON.").action((directory, rawOptions) => {
|
|
201
|
+
runCli(normalizeDepsCliOptions(directory, rawOptions));
|
|
202
|
+
});
|
|
203
|
+
cli.command("import [entry-file]", "Analyze an entry file and print its dependency tree.").usage("import <entry-file> [options]").option("--entry <path>", "Entry file to analyze.").option("--cwd <path>", "Working directory used for relative paths.").option("--config <path>", "Explicit tsconfig.json or jsconfig.json path.").option("--include-externals", "Include packages and Node built-ins in the tree.").option("--no-workspaces", "Do not expand sibling workspace packages into source subtrees.").option("--project-only", "Restrict traversal to the active tsconfig.json or jsconfig.json project.").option("--no-unused", "Omit imports that are never referenced.").option("--json", "Print the dependency tree as JSON.").action((entryFile, rawOptions) => {
|
|
204
|
+
runCli(normalizeImportCliOptions(entryFile, rawOptions));
|
|
205
|
+
});
|
|
206
|
+
cli.command("react [entry-file]", "Analyze React usage from an entry file.").usage("react [entry-file] [options]").option("--cwd <path>", "Working directory used for relative paths.").option("--config <path>", "Explicit tsconfig.json or jsconfig.json path.").option("--nextjs", "Infer Next.js page entries from app/ and pages/ when no entry is provided.").option("--filter <mode>", "Limit output to `component`, `hook`, or `builtin` usages.").option("--builtin", "Include built-in HTML nodes in the React tree.").option("--no-workspaces", "Do not expand sibling workspace packages into source subtrees.").option("--project-only", "Restrict traversal to the active tsconfig.json or jsconfig.json project.").option("--json", "Print the React usage tree as JSON.").action((entryFile, rawOptions) => {
|
|
207
|
+
runCli(normalizeReactCliOptions(entryFile, rawOptions));
|
|
208
|
+
});
|
|
209
|
+
cli.help();
|
|
210
|
+
cli.version(this.version);
|
|
211
|
+
return cli;
|
|
212
|
+
}
|
|
213
|
+
validateCommandSyntax() {
|
|
214
|
+
if (this.argv.length === 0) return;
|
|
215
|
+
const firstArgument = this.argv[0];
|
|
216
|
+
if (firstArgument === void 0) return;
|
|
217
|
+
if (firstArgument === "--react" || firstArgument.startsWith("--react=")) throw new Error("Unknown option `--react`");
|
|
218
|
+
if (firstArgument.startsWith("-")) return;
|
|
219
|
+
if (firstArgument === "deps" || firstArgument === "import" || firstArgument === "react") return;
|
|
220
|
+
throw new Error(`Unknown command \`${firstArgument}\``);
|
|
221
|
+
}
|
|
222
|
+
toProcessArgv() {
|
|
223
|
+
return [
|
|
224
|
+
"node",
|
|
225
|
+
"foresthouse",
|
|
226
|
+
...this.argv
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
function normalizeDepsCliOptions(directory, options) {
|
|
96
231
|
return {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
232
|
+
command: "deps",
|
|
233
|
+
directory,
|
|
234
|
+
diff: options.diff,
|
|
235
|
+
cwd: void 0,
|
|
236
|
+
configPath: void 0,
|
|
237
|
+
expandWorkspaces: true,
|
|
238
|
+
projectOnly: false,
|
|
239
|
+
json: options.json === true
|
|
103
240
|
};
|
|
104
241
|
}
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
242
|
+
function normalizeImportCliOptions(entryFile, options) {
|
|
243
|
+
return {
|
|
244
|
+
command: "import",
|
|
245
|
+
entryFile: resolveImportEntryFile(entryFile, options.entry),
|
|
246
|
+
cwd: options.cwd,
|
|
247
|
+
configPath: options.config,
|
|
248
|
+
expandWorkspaces: options.workspaces !== false,
|
|
249
|
+
projectOnly: options.projectOnly === true,
|
|
250
|
+
includeExternals: options.includeExternals === true,
|
|
251
|
+
omitUnused: options.unused === false,
|
|
252
|
+
json: options.json === true
|
|
253
|
+
};
|
|
109
254
|
}
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
255
|
+
function normalizeReactCliOptions(entryFile, options) {
|
|
256
|
+
return {
|
|
257
|
+
command: "react",
|
|
258
|
+
entryFile: resolveReactEntryFile(entryFile, options.nextjs),
|
|
259
|
+
cwd: options.cwd,
|
|
260
|
+
configPath: options.config,
|
|
261
|
+
expandWorkspaces: options.workspaces !== false,
|
|
262
|
+
projectOnly: options.projectOnly === true,
|
|
263
|
+
json: options.json === true,
|
|
264
|
+
filter: normalizeReactFilter(options.filter),
|
|
265
|
+
nextjs: options.nextjs === true,
|
|
266
|
+
includeBuiltins: options.builtin === true
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function resolveImportEntryFile(positionalEntryFile, optionEntryFile) {
|
|
270
|
+
if (positionalEntryFile !== void 0 && optionEntryFile !== void 0 && positionalEntryFile !== optionEntryFile) throw new Error("Provide the import entry only once, either as `foresthouse import <entry-file>` or `foresthouse import --entry <path>`.");
|
|
271
|
+
const resolvedEntryFile = positionalEntryFile ?? optionEntryFile;
|
|
272
|
+
if (resolvedEntryFile === void 0) throw new Error("Missing import entry file. Use `foresthouse import <entry-file>` or `foresthouse import --entry <path>`.");
|
|
273
|
+
return resolvedEntryFile;
|
|
274
|
+
}
|
|
275
|
+
function normalizeReactFilter(filter) {
|
|
276
|
+
if (filter === void 0) return "all";
|
|
277
|
+
if (filter === "component" || filter === "hook" || filter === "builtin") return filter;
|
|
278
|
+
throw new Error(`Unknown React filter: ${filter}`);
|
|
125
279
|
}
|
|
126
|
-
|
|
280
|
+
function resolveReactEntryFile(entryFile, nextjs) {
|
|
281
|
+
if (entryFile !== void 0) return entryFile;
|
|
282
|
+
if (nextjs === true) return;
|
|
283
|
+
throw new Error("Missing React entry file. Use `foresthouse react <entry-file>` or `foresthouse react --nextjs`.");
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/cli.ts
|
|
287
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
288
|
+
main(version);
|
|
127
289
|
//#endregion
|
|
128
290
|
export {};
|
|
129
291
|
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { createRequire } from 'node:module'\nimport process from 'node:process'\n\nimport { analyzeDependencies, graphToSerializableTree } from './analyzer.js'\nimport {\n analyzeReactUsage,\n graphToSerializableReactTree,\n} from './react-analyzer.js'\nimport { printReactUsageTree } from './react-tree.js'\nimport { printDependencyTree } from './tree.js'\nimport type { ReactUsageFilter } from './types.js'\n\nconst require = createRequire(import.meta.url)\nconst { version } = require('../package.json') as { version: string }\n\ninterface CliOptions {\n readonly entryFile: string\n readonly cwd: string | undefined\n readonly configPath: string | undefined\n readonly includeExternals: boolean\n readonly json: boolean\n readonly react: ReactUsageFilter | undefined\n}\n\nfunction main(): void {\n try {\n const options = parseArgs(process.argv.slice(2))\n\n if (options.react !== undefined) {\n const graph = analyzeReactUsage(options.entryFile, {\n ...(options.cwd === undefined ? {} : { cwd: options.cwd }),\n ...(options.configPath === undefined\n ? {}\n : { configPath: options.configPath }),\n })\n\n if (options.json) {\n process.stdout.write(\n `${JSON.stringify(\n graphToSerializableReactTree(graph, {\n filter: options.react,\n }),\n null,\n 2,\n )}\\n`,\n )\n return\n }\n\n process.stdout.write(\n `${printReactUsageTree(graph, {\n cwd: options.cwd ?? graph.cwd,\n filter: options.react,\n })}\\n`,\n )\n return\n }\n\n const graph = analyzeDependencies(options.entryFile, {\n ...(options.cwd === undefined ? {} : { cwd: options.cwd }),\n ...(options.configPath === undefined\n ? {}\n : { configPath: options.configPath }),\n })\n\n if (options.json) {\n process.stdout.write(\n `${JSON.stringify(graphToSerializableTree(graph), null, 2)}\\n`,\n )\n return\n }\n\n process.stdout.write(\n `${printDependencyTree(graph, {\n cwd: options.cwd ?? graph.cwd,\n includeExternals: options.includeExternals,\n })}\\n`,\n )\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Unknown error occurred.'\n process.stderr.write(`foresthouse: ${message}\\n`)\n process.exitCode = 1\n }\n}\n\nfunction parseArgs(argv: string[]): CliOptions {\n if (argv.includes('--version') || argv.includes('-v')) {\n process.stdout.write(`${version}\\n`)\n process.exit(0)\n }\n\n if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {\n printHelp()\n process.exit(0)\n }\n\n let entryFile: string | undefined\n let cwd: string | undefined\n let configPath: string | undefined\n let includeExternals = false\n let json = false\n let react: ReactUsageFilter | undefined\n\n for (let index = 0; index < argv.length; index += 1) {\n const argument = argv[index]\n if (argument === undefined) {\n continue\n }\n\n if (argument === '--cwd') {\n cwd = readOptionValue(argv, index, '--cwd')\n index += 1\n continue\n }\n\n if (argument === '--config') {\n configPath = readOptionValue(argv, index, '--config')\n index += 1\n continue\n }\n\n if (argument === '--include-externals') {\n includeExternals = true\n continue\n }\n\n if (argument === '--json') {\n json = true\n continue\n }\n\n if (argument === '--react') {\n react = 'all'\n continue\n }\n\n if (argument.startsWith('--react=')) {\n const value = argument.slice('--react='.length)\n if (value === 'component' || value === 'hook') {\n react = value\n continue\n }\n\n throw new Error(`Unknown React mode: ${value}`)\n }\n\n if (argument.startsWith('-')) {\n throw new Error(`Unknown option: ${argument}`)\n }\n\n if (entryFile !== undefined) {\n throw new Error('Only one entry file can be provided.')\n }\n\n entryFile = argument\n }\n\n if (entryFile === undefined) {\n throw new Error('An entry file is required.')\n }\n\n return {\n entryFile,\n cwd,\n configPath,\n includeExternals,\n json,\n react,\n }\n}\n\nfunction readOptionValue(\n argv: string[],\n index: number,\n optionName: string,\n): string {\n const value = argv[index + 1]\n if (value === undefined || value.startsWith('-')) {\n throw new Error(`Missing value for ${optionName}`)\n }\n\n return value\n}\n\nfunction printHelp(): void {\n process.stdout.write(`foresthouse v${version}\n\nUsage:\n foresthouse <entry-file> [options]\n\nOptions:\n --cwd <path> Working directory used for relative paths.\n --config <path> Explicit tsconfig.json or jsconfig.json path.\n --include-externals Include packages and Node built-ins in the tree.\n --react[=component|hook] Print a React usage tree instead of the import tree.\n --json Print the dependency tree as JSON.\n -v, --version Show the current version.\n -h, --help Show this help message.\n`)\n}\n\nmain()\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["process"],"sources":["../src/commands/base.ts","../src/commands/deps.ts","../src/commands/import.ts","../src/app/react-entry-files.ts","../src/commands/react.ts","../src/app/run.ts","../src/app/cli.ts","../src/cli.ts"],"sourcesContent":["import type { CliOptions } from '../app/args.js'\nimport type { AnalyzeOptions } from '../types/analyze-options.js'\n\nexport abstract class BaseCommand<\n TGraph,\n TOptions extends CliOptions = CliOptions,\n> {\n constructor(protected readonly options: TOptions) {}\n\n run(): void {\n const graph = this.analyze()\n\n if (this.isJsonMode()) {\n process.stdout.write(\n `${JSON.stringify(this.serialize(graph), null, 2)}\\n`,\n )\n return\n }\n\n process.stdout.write(`${this.render(graph)}\\n`)\n }\n\n protected isJsonMode(): boolean {\n return this.options.json\n }\n\n protected getAnalyzeOptions(): AnalyzeOptions {\n return {\n ...(this.options.cwd === undefined ? {} : { cwd: this.options.cwd }),\n ...(this.options.configPath === undefined\n ? {}\n : { configPath: this.options.configPath }),\n expandWorkspaces: this.options.expandWorkspaces,\n projectOnly: this.options.projectOnly,\n }\n }\n\n protected abstract analyze(): TGraph\n protected abstract serialize(graph: TGraph): object\n protected abstract render(graph: TGraph): string\n}\n","import { analyzePackageDependencyDiff } from '../analyzers/deps/diff.js'\nimport { analyzePackageDependencies } from '../analyzers/deps/index.js'\nimport type { DepsCliOptions } from '../app/args.js'\nimport {\n printPackageDependencyDiffTree,\n printPackageDependencyTree,\n} from '../output/ascii/deps.js'\nimport {\n diffGraphToSerializablePackageTree,\n graphToSerializablePackageTree,\n} from '../output/json/deps.js'\nimport type { PackageDependencyDiffGraph } from '../types/package-dependency-diff-graph.js'\nimport type { PackageDependencyGraph } from '../types/package-dependency-graph.js'\nimport { BaseCommand } from './base.js'\n\nexport class DepsCommand extends BaseCommand<\n PackageDependencyGraph | PackageDependencyDiffGraph,\n DepsCliOptions\n> {\n protected analyze(): PackageDependencyGraph | PackageDependencyDiffGraph {\n if (this.options.diff !== undefined) {\n return analyzePackageDependencyDiff(\n this.options.directory,\n this.options.diff,\n )\n }\n\n return analyzePackageDependencies(this.options.directory)\n }\n\n protected serialize(\n graph: PackageDependencyGraph | PackageDependencyDiffGraph,\n ): object {\n if (isPackageDependencyDiffGraph(graph)) {\n return diffGraphToSerializablePackageTree(graph)\n }\n\n return graphToSerializablePackageTree(graph)\n }\n\n protected render(\n graph: PackageDependencyGraph | PackageDependencyDiffGraph,\n ): string {\n if (isPackageDependencyDiffGraph(graph)) {\n return printPackageDependencyDiffTree(graph)\n }\n\n return printPackageDependencyTree(graph)\n }\n}\n\nfunction isPackageDependencyDiffGraph(\n graph: PackageDependencyGraph | PackageDependencyDiffGraph,\n): graph is PackageDependencyDiffGraph {\n return 'root' in graph\n}\n","import { analyzeDependencies } from '../analyzers/import/index.js'\nimport type { ImportCliOptions } from '../app/args.js'\nimport { printDependencyTree } from '../output/ascii/import.js'\nimport { graphToSerializableTree } from '../output/json/import.js'\nimport type { DependencyGraph } from '../types/dependency-graph.js'\nimport { BaseCommand } from './base.js'\n\nexport class ImportCommand extends BaseCommand<\n DependencyGraph,\n ImportCliOptions\n> {\n protected analyze(): DependencyGraph {\n return analyzeDependencies(this.options.entryFile, this.getAnalyzeOptions())\n }\n\n protected serialize(graph: DependencyGraph): object {\n return graphToSerializableTree(graph, {\n omitUnused: this.options.omitUnused,\n })\n }\n\n protected render(graph: DependencyGraph): string {\n return printDependencyTree(graph, {\n cwd: this.options.cwd ?? graph.cwd,\n includeExternals: this.options.includeExternals,\n omitUnused: this.options.omitUnused,\n })\n }\n}\n","import fs from 'node:fs'\nimport path from 'node:path'\nimport { isSourceCodeFile } from '../utils/is-source-code-file.js'\nimport type { ReactCliOptions } from './args.js'\n\nconst NEXT_JS_ENTRY_ROOTS = [\n 'pages',\n 'app',\n path.join('src', 'pages'),\n path.join('src', 'app'),\n]\n\nexport function resolveReactEntryFiles(options: ReactCliOptions): string[] {\n if (options.entryFile !== undefined) {\n return [options.entryFile]\n }\n\n if (!options.nextjs) {\n throw new Error(\n 'Missing React entry file. Use `foresthouse react <entry-file>` or `foresthouse react --nextjs`.',\n )\n }\n\n return discoverNextJsPageEntries(options.cwd)\n}\n\nexport function discoverNextJsPageEntries(cwd?: string): string[] {\n const effectiveCwd = path.resolve(cwd ?? process.cwd())\n const entries = new Set<string>()\n\n collectPagesRouterEntries(\n path.join(effectiveCwd, 'pages'),\n effectiveCwd,\n entries,\n )\n collectAppRouterEntries(path.join(effectiveCwd, 'app'), effectiveCwd, entries)\n collectPagesRouterEntries(\n path.join(effectiveCwd, 'src', 'pages'),\n effectiveCwd,\n entries,\n )\n collectAppRouterEntries(\n path.join(effectiveCwd, 'src', 'app'),\n effectiveCwd,\n entries,\n )\n\n const resolvedEntries = [...entries].sort()\n if (resolvedEntries.length > 0) {\n return resolvedEntries\n }\n\n const searchedRoots = NEXT_JS_ENTRY_ROOTS.map((root) => `\\`${root}/\\``).join(\n ', ',\n )\n throw new Error(\n `No Next.js page entries found. Searched ${searchedRoots} relative to ${effectiveCwd}.`,\n )\n}\n\nfunction collectPagesRouterEntries(\n rootDirectory: string,\n cwd: string,\n entries: Set<string>,\n): void {\n walkDirectory(rootDirectory, (filePath, relativePath) => {\n if (!isSourceCodeFile(filePath) || filePath.endsWith('.d.ts')) {\n return\n }\n\n const segments = relativePath.split(path.sep)\n if (segments[0] === 'api') {\n return\n }\n\n const baseName = path.basename(filePath)\n if (baseName.startsWith('_')) {\n return\n }\n\n entries.add(path.relative(cwd, filePath))\n })\n}\n\nfunction collectAppRouterEntries(\n rootDirectory: string,\n cwd: string,\n entries: Set<string>,\n): void {\n walkDirectory(rootDirectory, (filePath) => {\n if (!isSourceCodeFile(filePath) || filePath.endsWith('.d.ts')) {\n return\n }\n\n const extension = path.extname(filePath)\n const baseName = path.basename(filePath, extension)\n if (baseName !== 'page') {\n return\n }\n\n entries.add(path.relative(cwd, filePath))\n })\n}\n\nfunction walkDirectory(\n rootDirectory: string,\n visitor: (filePath: string, relativePath: string) => void,\n): void {\n if (!fs.existsSync(rootDirectory)) {\n return\n }\n\n const stack = [rootDirectory]\n\n while (stack.length > 0) {\n const directory = stack.pop()\n if (directory === undefined) {\n continue\n }\n\n const directoryEntries = fs\n .readdirSync(directory, { withFileTypes: true })\n .sort((left, right) => left.name.localeCompare(right.name))\n\n for (let index = directoryEntries.length - 1; index >= 0; index -= 1) {\n const entry = directoryEntries[index]\n if (entry === undefined) {\n continue\n }\n\n const entryPath = path.join(directory, entry.name)\n if (entry.isDirectory()) {\n stack.push(entryPath)\n continue\n }\n\n if (!entry.isFile()) {\n continue\n }\n\n visitor(entryPath, path.relative(rootDirectory, entryPath))\n }\n }\n}\n","import { analyzeReactUsage } from '../analyzers/react/index.js'\nimport type { ReactCliOptions } from '../app/args.js'\nimport { resolveReactEntryFiles } from '../app/react-entry-files.js'\nimport { printReactUsageTree } from '../output/ascii/react.js'\nimport { graphToSerializableReactTree } from '../output/json/react.js'\nimport type { AnalyzeOptions } from '../types/analyze-options.js'\nimport type { ReactUsageFilter } from '../types/react-usage-filter.js'\nimport type { ReactUsageGraph } from '../types/react-usage-graph.js'\nimport { BaseCommand } from './base.js'\n\nexport class ReactCommand extends BaseCommand<\n ReactUsageGraph,\n ReactCliOptions\n> {\n protected analyze(): ReactUsageGraph {\n return analyzeReactUsage(\n resolveReactEntryFiles(this.options),\n this.getAnalyzeOptions(),\n )\n }\n\n protected serialize(graph: ReactUsageGraph): object {\n return graphToSerializableReactTree(graph, {\n filter: this.getFilter(),\n })\n }\n\n protected render(graph: ReactUsageGraph): string {\n return printReactUsageTree(graph, {\n cwd: this.options.cwd ?? graph.cwd,\n filter: this.getFilter(),\n })\n }\n\n protected getAnalyzeOptions(): AnalyzeOptions {\n return {\n ...super.getAnalyzeOptions(),\n includeBuiltins: this.options.includeBuiltins,\n }\n }\n\n private getFilter(): ReactUsageFilter {\n return this.options.filter\n }\n}\n","import { DepsCommand } from '../commands/deps.js'\nimport { ImportCommand } from '../commands/import.js'\nimport { ReactCommand } from '../commands/react.js'\nimport type { CliOptions } from './args.js'\n\nexport function runCli(options: CliOptions): void {\n new CliApplication(options).run()\n}\n\nclass CliApplication {\n constructor(private readonly options: CliOptions) {}\n\n run(): void {\n this.createCommand().run()\n }\n\n private createCommand(): {\n run(): void\n } {\n if (this.options.command === 'deps') {\n return new DepsCommand(this.options)\n }\n\n if (this.options.command === 'react') {\n return new ReactCommand(this.options)\n }\n\n return new ImportCommand(this.options)\n }\n}\n","import process from 'node:process'\n\nimport { cac } from 'cac'\n\nimport type { ReactUsageFilter } from '../types/react-usage-filter.js'\nimport type {\n DepsCliOptions,\n ImportCliOptions,\n ReactCliOptions,\n} from './args.js'\nimport { runCli } from './run.js'\n\nexport function main(version: string, argv = process.argv.slice(2)): void {\n new CliMain(version, argv).run()\n}\n\nclass CliMain {\n constructor(\n private readonly version: string,\n private readonly argv: string[],\n ) {}\n\n run(): void {\n try {\n const cli = this.createCli()\n\n if (this.argv.length === 0) {\n cli.outputHelp()\n return\n }\n\n this.validateCommandSyntax()\n cli.parse(this.toProcessArgv())\n } catch (error) {\n const message =\n error instanceof Error ? error.message : 'Unknown error occurred.'\n process.stderr.write(`foresthouse: ${message}\\n`)\n process.exitCode = 1\n }\n }\n\n private createCli() {\n const cli = cac('foresthouse')\n\n cli\n .command(\n 'deps <directory>',\n 'Analyze package.json dependencies from a package directory.',\n )\n .usage('deps <directory> [options]')\n .option(\n '--diff <git-ref-or-range>',\n 'Show only dependency-tree changes relative to a Git revision or range.',\n )\n .option('--json', 'Print the package tree as JSON.')\n .action((directory: string, rawOptions: ParsedDepsCliOptions) => {\n runCli(normalizeDepsCliOptions(directory, rawOptions))\n })\n\n cli\n .command(\n 'import [entry-file]',\n 'Analyze an entry file and print its dependency tree.',\n )\n .usage('import <entry-file> [options]')\n .option('--entry <path>', 'Entry file to analyze.')\n .option('--cwd <path>', 'Working directory used for relative paths.')\n .option(\n '--config <path>',\n 'Explicit tsconfig.json or jsconfig.json path.',\n )\n .option(\n '--include-externals',\n 'Include packages and Node built-ins in the tree.',\n )\n .option(\n '--no-workspaces',\n 'Do not expand sibling workspace packages into source subtrees.',\n )\n .option(\n '--project-only',\n 'Restrict traversal to the active tsconfig.json or jsconfig.json project.',\n )\n .option('--no-unused', 'Omit imports that are never referenced.')\n .option('--json', 'Print the dependency tree as JSON.')\n .action(\n (entryFile: string | undefined, rawOptions: ParsedImportCliOptions) => {\n runCli(normalizeImportCliOptions(entryFile, rawOptions))\n },\n )\n\n cli\n .command('react [entry-file]', 'Analyze React usage from an entry file.')\n .usage('react [entry-file] [options]')\n .option('--cwd <path>', 'Working directory used for relative paths.')\n .option(\n '--config <path>',\n 'Explicit tsconfig.json or jsconfig.json path.',\n )\n .option(\n '--nextjs',\n 'Infer Next.js page entries from app/ and pages/ when no entry is provided.',\n )\n .option(\n '--filter <mode>',\n 'Limit output to `component`, `hook`, or `builtin` usages.',\n )\n .option('--builtin', 'Include built-in HTML nodes in the React tree.')\n .option(\n '--no-workspaces',\n 'Do not expand sibling workspace packages into source subtrees.',\n )\n .option(\n '--project-only',\n 'Restrict traversal to the active tsconfig.json or jsconfig.json project.',\n )\n .option('--json', 'Print the React usage tree as JSON.')\n .action(\n (entryFile: string | undefined, rawOptions: ParsedReactCliOptions) => {\n runCli(normalizeReactCliOptions(entryFile, rawOptions))\n },\n )\n\n cli.help()\n cli.version(this.version)\n\n return cli\n }\n\n private validateCommandSyntax(): void {\n if (this.argv.length === 0) {\n return\n }\n\n const firstArgument = this.argv[0]\n\n if (firstArgument === undefined) {\n return\n }\n\n if (firstArgument === '--react' || firstArgument.startsWith('--react=')) {\n throw new Error('Unknown option `--react`')\n }\n\n if (firstArgument.startsWith('-')) {\n return\n }\n\n if (\n firstArgument === 'deps' ||\n firstArgument === 'import' ||\n firstArgument === 'react'\n ) {\n return\n }\n\n throw new Error(`Unknown command \\`${firstArgument}\\``)\n }\n\n private toProcessArgv(): string[] {\n return ['node', 'foresthouse', ...this.argv]\n }\n}\n\ninterface ParsedBaseCliOptions {\n readonly cwd?: string\n readonly config?: string\n readonly workspaces?: boolean\n readonly projectOnly?: boolean\n readonly json?: boolean\n}\n\ninterface ParsedDepsCliOptions {\n readonly diff?: string\n readonly json?: boolean\n}\n\ninterface ParsedImportCliOptions extends ParsedBaseCliOptions {\n readonly entry?: string\n readonly includeExternals?: boolean\n readonly unused?: boolean\n}\n\ninterface ParsedReactCliOptions extends ParsedBaseCliOptions {\n readonly filter?: string\n readonly nextjs?: boolean\n readonly builtin?: boolean\n}\n\nfunction normalizeDepsCliOptions(\n directory: string,\n options: ParsedDepsCliOptions,\n): DepsCliOptions {\n return {\n command: 'deps',\n directory,\n diff: options.diff,\n cwd: undefined,\n configPath: undefined,\n expandWorkspaces: true,\n projectOnly: false,\n json: options.json === true,\n }\n}\n\nfunction normalizeImportCliOptions(\n entryFile: string | undefined,\n options: ParsedImportCliOptions,\n): ImportCliOptions {\n return {\n command: 'import',\n entryFile: resolveImportEntryFile(entryFile, options.entry),\n cwd: options.cwd,\n configPath: options.config,\n expandWorkspaces: options.workspaces !== false,\n projectOnly: options.projectOnly === true,\n includeExternals: options.includeExternals === true,\n omitUnused: options.unused === false,\n json: options.json === true,\n }\n}\n\nfunction normalizeReactCliOptions(\n entryFile: string | undefined,\n options: ParsedReactCliOptions,\n): ReactCliOptions {\n return {\n command: 'react',\n entryFile: resolveReactEntryFile(entryFile, options.nextjs),\n cwd: options.cwd,\n configPath: options.config,\n expandWorkspaces: options.workspaces !== false,\n projectOnly: options.projectOnly === true,\n json: options.json === true,\n filter: normalizeReactFilter(options.filter),\n nextjs: options.nextjs === true,\n includeBuiltins: options.builtin === true,\n }\n}\n\nfunction resolveImportEntryFile(\n positionalEntryFile: string | undefined,\n optionEntryFile: string | undefined,\n): string {\n if (\n positionalEntryFile !== undefined &&\n optionEntryFile !== undefined &&\n positionalEntryFile !== optionEntryFile\n ) {\n throw new Error(\n 'Provide the import entry only once, either as `foresthouse import <entry-file>` or `foresthouse import --entry <path>`.',\n )\n }\n\n const resolvedEntryFile = positionalEntryFile ?? optionEntryFile\n\n if (resolvedEntryFile === undefined) {\n throw new Error(\n 'Missing import entry file. Use `foresthouse import <entry-file>` or `foresthouse import --entry <path>`.',\n )\n }\n\n return resolvedEntryFile\n}\n\nfunction normalizeReactFilter(\n filter: ParsedReactCliOptions['filter'],\n): ReactUsageFilter {\n if (filter === undefined) {\n return 'all'\n }\n\n if (filter === 'component' || filter === 'hook' || filter === 'builtin') {\n return filter\n }\n\n throw new Error(`Unknown React filter: ${filter}`)\n}\n\nfunction resolveReactEntryFile(\n entryFile: string | undefined,\n nextjs: boolean | undefined,\n): string | undefined {\n if (entryFile !== undefined) {\n return entryFile\n }\n\n if (nextjs === true) {\n return undefined\n }\n\n throw new Error(\n 'Missing React entry file. Use `foresthouse react <entry-file>` or `foresthouse react --nextjs`.',\n )\n}\n","#!/usr/bin/env node\n\nimport { createRequire } from 'node:module'\n\nimport { main } from './app/cli.js'\n\nconst require = createRequire(import.meta.url)\nconst { version } = require('../package.json') as { version: string }\n\nmain(version)\n"],"mappings":"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"}
|