@zeroheight/adoption-cli 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/ast/analyze.d.ts +14 -0
- package/dist/ast/analyze.js +82 -0
- package/dist/ast/parser.d.ts +11 -0
- package/dist/ast/parser.js +23 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +22 -0
- package/dist/commands/analyze.d.ts +12 -0
- package/dist/commands/analyze.js +26 -0
- package/dist/commands/analyze.utils.d.ts +26 -0
- package/dist/commands/analyze.utils.js +126 -0
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.js +51 -0
- package/dist/common/config.d.ts +11 -0
- package/dist/common/config.js +32 -0
- package/dist/components/analyze/analyze.d.ts +10 -0
- package/dist/components/analyze/analyze.js +107 -0
- package/dist/components/auth/credentials-already-exists.d.ts +16 -0
- package/dist/components/auth/credentials-already-exists.js +31 -0
- package/dist/components/auth/credentials-preview.d.ts +10 -0
- package/dist/components/auth/credentials-preview.js +20 -0
- package/dist/components/auth/no-credentials-onboarding.d.ts +7 -0
- package/dist/components/auth/no-credentials-onboarding.js +45 -0
- package/dist/components/help-info.d.ts +5 -0
- package/dist/components/help-info.js +24 -0
- package/dist/components/ui/confirm-input.d.ts +10 -0
- package/dist/components/ui/confirm-input.js +10 -0
- package/dist/components/ui/continue-prompt.d.ts +6 -0
- package/dist/components/ui/continue-prompt.js +13 -0
- package/dist/components/usage-table.d.ts +7 -0
- package/dist/components/usage-table.js +14 -0
- package/package.json +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @zeroheight/adoption-cli
|
|
2
|
+
|
|
3
|
+
CLI for measuring component usage
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i @zeroheight/adoption-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
In the repository in which you wish to analyze the component usage, run the following command:
|
|
13
|
+
```
|
|
14
|
+
zh-adoption analyze
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
To send adoption data to your zeroheight account you will need to authenticate. This can be done as part of the `analyze` flow or separetly by running:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
zh-adoption auth
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
More info on the commands can be seen by running
|
|
24
|
+
```
|
|
25
|
+
zh-adoption --help
|
|
26
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Node } from "acorn";
|
|
2
|
+
export type VisitorState = {
|
|
3
|
+
components: Map<string, number>;
|
|
4
|
+
imports: Map<string, {
|
|
5
|
+
package: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}>;
|
|
8
|
+
};
|
|
9
|
+
export type RawUsage = {
|
|
10
|
+
name: string;
|
|
11
|
+
count: number;
|
|
12
|
+
package: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function analyze(ast: Node): RawUsage[];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as walk from "acorn-walk";
|
|
2
|
+
function transformVisitorStateToRawUsage(visitorState) {
|
|
3
|
+
const transformedUsageMap = new Map();
|
|
4
|
+
for (const [name, count] of visitorState.components) {
|
|
5
|
+
const importInfo = visitorState.imports.get(name);
|
|
6
|
+
const actualName = importInfo?.name ?? name;
|
|
7
|
+
const packageName = importInfo?.package ?? "";
|
|
8
|
+
const key = `package:${packageName} name:${actualName}`;
|
|
9
|
+
const currentValue = transformedUsageMap.get(key) ?? {
|
|
10
|
+
name: actualName,
|
|
11
|
+
count: 0,
|
|
12
|
+
package: importInfo?.package ?? "",
|
|
13
|
+
};
|
|
14
|
+
transformedUsageMap.set(key, {
|
|
15
|
+
...currentValue,
|
|
16
|
+
count: currentValue.count + count,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return Array.from(transformedUsageMap.values());
|
|
20
|
+
}
|
|
21
|
+
export function analyze(ast) {
|
|
22
|
+
const visitorState = {
|
|
23
|
+
components: new Map(),
|
|
24
|
+
imports: new Map(),
|
|
25
|
+
};
|
|
26
|
+
const visitors = {
|
|
27
|
+
JSXElement(node, state, _nodePath) {
|
|
28
|
+
const el = node.openingElement;
|
|
29
|
+
const elName = el.name.name;
|
|
30
|
+
// Ignore html tags e.g. div, h1, span
|
|
31
|
+
if (elName[0] === elName[0]?.toLocaleLowerCase())
|
|
32
|
+
return;
|
|
33
|
+
state.components.set(elName, (state.components.get(elName) ?? 0) + 1);
|
|
34
|
+
},
|
|
35
|
+
ImportDeclaration(node, state, _nodePath) {
|
|
36
|
+
const packageName = node.source.value;
|
|
37
|
+
node.specifiers.forEach((specifier) => {
|
|
38
|
+
// not handling namespace imports as we can't determine the actual name
|
|
39
|
+
// e.g. import * as MyIconLibrary from 'my-icon-library'
|
|
40
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let name = specifier.local.name;
|
|
44
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
45
|
+
// handles the following cases:
|
|
46
|
+
// import Icon from 'my-icon-library'
|
|
47
|
+
name = specifier.local.name;
|
|
48
|
+
}
|
|
49
|
+
else if (specifier.type === "ImportSpecifier") {
|
|
50
|
+
// handles the following cases:
|
|
51
|
+
// import { Icon } from 'my-icon-library'
|
|
52
|
+
// import { Icon as MyIcon } from 'my-icon-library'
|
|
53
|
+
name =
|
|
54
|
+
specifier.imported.type === "Literal"
|
|
55
|
+
? specifier.imported.value
|
|
56
|
+
: specifier.imported.name;
|
|
57
|
+
}
|
|
58
|
+
state.imports.set(specifier.local.name, {
|
|
59
|
+
package: packageName,
|
|
60
|
+
name,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const base = {
|
|
66
|
+
...walk.base,
|
|
67
|
+
TSInterfaceDeclaration() { },
|
|
68
|
+
TSModuleDeclaration() { },
|
|
69
|
+
TSAsExpression() { },
|
|
70
|
+
TSDeclareFunction() { },
|
|
71
|
+
TSTypeAliasDeclaration() { },
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
walk.ancestor(ast, visitors, base, visitorState);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
console.log("error", e);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
return transformVisitorStateToRawUsage(visitorState);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as acorn from "acorn";
|
|
2
|
+
/**
|
|
3
|
+
* Parse code and return AST
|
|
4
|
+
*
|
|
5
|
+
* This extends the acorn parser with support for TypeScript and JSX
|
|
6
|
+
*
|
|
7
|
+
* @param code The string containing JS/TS
|
|
8
|
+
* @param version a version of ECMA Script to use, defaults to latest
|
|
9
|
+
* @returns Parsed AST
|
|
10
|
+
*/
|
|
11
|
+
export declare function parse(code: string, version?: acorn.ecmaVersion): acorn.Program;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as acorn from "acorn";
|
|
2
|
+
import { tsPlugin } from "acorn-typescript";
|
|
3
|
+
import * as walk from "acorn-walk";
|
|
4
|
+
import { extend } from "acorn-jsx-walk";
|
|
5
|
+
/**
|
|
6
|
+
* Parse code and return AST
|
|
7
|
+
*
|
|
8
|
+
* This extends the acorn parser with support for TypeScript and JSX
|
|
9
|
+
*
|
|
10
|
+
* @param code The string containing JS/TS
|
|
11
|
+
* @param version a version of ECMA Script to use, defaults to latest
|
|
12
|
+
* @returns Parsed AST
|
|
13
|
+
*/
|
|
14
|
+
export function parse(code, version = "latest") {
|
|
15
|
+
const typescriptPlugin = tsPlugin();
|
|
16
|
+
extend(walk.base);
|
|
17
|
+
const JSXParser = acorn.Parser.extend(typescriptPlugin);
|
|
18
|
+
return JSXParser.parse(code, {
|
|
19
|
+
ecmaVersion: version,
|
|
20
|
+
sourceType: "module",
|
|
21
|
+
locations: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { render } from "ink-render-string";
|
|
5
|
+
import { analyzeCommand } from "./commands/analyze.js";
|
|
6
|
+
import { authCommand } from "./commands/auth.js";
|
|
7
|
+
import HelpInfo from "./components/help-info.js";
|
|
8
|
+
const program = new Command();
|
|
9
|
+
const { output, cleanup } = render(React.createElement(HelpInfo, null));
|
|
10
|
+
program
|
|
11
|
+
.name("zh-adoption")
|
|
12
|
+
.description("CLI for measuring design system usage usage in your products")
|
|
13
|
+
.version("0.1.0")
|
|
14
|
+
.addHelpText("before", output)
|
|
15
|
+
.addCommand(analyzeCommand())
|
|
16
|
+
.addCommand(authCommand());
|
|
17
|
+
cleanup();
|
|
18
|
+
// Only start parsing if run as CLI, don't start parsing during testing
|
|
19
|
+
if (process.env["NODE_ENV"] !== "test") {
|
|
20
|
+
program.parse();
|
|
21
|
+
}
|
|
22
|
+
export default program;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RenderOptions } from "ink";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { RawUsage } from "../ast/analyze.js";
|
|
4
|
+
interface AnalyzeOptions {
|
|
5
|
+
extensions: string;
|
|
6
|
+
ignore: string;
|
|
7
|
+
dryRun: boolean;
|
|
8
|
+
}
|
|
9
|
+
export type RawUsageMap = Map<string, RawUsage[]>;
|
|
10
|
+
export declare function analyzeAction(options: AnalyzeOptions, renderOptions?: RenderOptions): Promise<void>;
|
|
11
|
+
export declare function analyzeCommand(): Command;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { Command, Option } from "commander";
|
|
4
|
+
import Analyze from "../components/analyze/analyze.js";
|
|
5
|
+
import { analyzeFiles } from "./analyze.utils.js";
|
|
6
|
+
export async function analyzeAction(options, renderOptions) {
|
|
7
|
+
render(React.createElement(Analyze, { onAnalyzeFiles: () => analyzeFiles(options.extensions, options.ignore), dryRun: options.dryRun }), renderOptions);
|
|
8
|
+
}
|
|
9
|
+
export function analyzeCommand() {
|
|
10
|
+
const command = new Command();
|
|
11
|
+
return command
|
|
12
|
+
.command("analyze")
|
|
13
|
+
.description("Analyze your codebase to determine component usage metrics")
|
|
14
|
+
.addOption(new Option("-e, --extensions [ext]", "file extensions to include when searching for components").default("**/*.{js,jsx,ts,tsx}", "glob pattern to determine file extensions"))
|
|
15
|
+
.addOption(new Option("-i, --ignore", "files to ignore when searching for components").default("**/*.{test,spec}.*", "glob pattern to determine ignored files"))
|
|
16
|
+
.addOption(new Option("-d, --dry-run", "don't push results to zeroheight").default(false))
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
try {
|
|
19
|
+
await analyzeAction(options);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
console.error(e);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RawUsageMap } from "./analyze.js";
|
|
2
|
+
import { Credentials } from "../common/config.js";
|
|
3
|
+
export interface ComponentUsageRecord {
|
|
4
|
+
name: string;
|
|
5
|
+
files: string[];
|
|
6
|
+
count: number;
|
|
7
|
+
package: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Get a list of files matching extensions, skips hidden files and node_modules
|
|
11
|
+
* @param base starting directory
|
|
12
|
+
* @param extensions glob syntax of files to look for, e.g. **\/*.{js,jsx,ts,tsx}
|
|
13
|
+
* @param ignorePattern glob syntax of files to ignore, e.g. **\/*.{test,spec}.*
|
|
14
|
+
* @param gitIgnore contents of .gitignore file
|
|
15
|
+
* @returns list of file paths
|
|
16
|
+
*/
|
|
17
|
+
export declare function findFiles(base: string, extensions: string, ignorePattern: string): Promise<string[]>;
|
|
18
|
+
export declare function getZeroheightURL(): URL;
|
|
19
|
+
/**
|
|
20
|
+
* Post usage data to zeroheight to store
|
|
21
|
+
*/
|
|
22
|
+
export declare function submitUsageData(usage: RawUsageMap, credentials: Credentials): Promise<any>;
|
|
23
|
+
export declare function analyzeFiles(extensions: string, ignorePattern: string): Promise<{
|
|
24
|
+
errors: string[];
|
|
25
|
+
usage: RawUsageMap;
|
|
26
|
+
}>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { stat, readFile } from "fs/promises";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { Glob } from "glob";
|
|
5
|
+
import ignore from "ignore";
|
|
6
|
+
import { parse } from "../ast/parser.js";
|
|
7
|
+
import { analyze } from "../ast/analyze.js";
|
|
8
|
+
/**
|
|
9
|
+
* Get a list of files matching extensions, skips hidden files and node_modules
|
|
10
|
+
* @param base starting directory
|
|
11
|
+
* @param extensions glob syntax of files to look for, e.g. **\/*.{js,jsx,ts,tsx}
|
|
12
|
+
* @param ignorePattern glob syntax of files to ignore, e.g. **\/*.{test,spec}.*
|
|
13
|
+
* @param gitIgnore contents of .gitignore file
|
|
14
|
+
* @returns list of file paths
|
|
15
|
+
*/
|
|
16
|
+
export async function findFiles(base, extensions, ignorePattern) {
|
|
17
|
+
const gitIgnore = await getGitIgnore(base);
|
|
18
|
+
// typescript complains about the ignore() function having
|
|
19
|
+
// no call signature. Could not find a way to fix this.
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
const ig = ignore()
|
|
22
|
+
// skip hidden files and directories
|
|
23
|
+
.add("/**/.*")
|
|
24
|
+
.add("/**/.*/")
|
|
25
|
+
// skip node_modules
|
|
26
|
+
.add("node_modules/")
|
|
27
|
+
// add gitignore rules
|
|
28
|
+
.add(gitIgnore);
|
|
29
|
+
const matchingFiles = [];
|
|
30
|
+
const g = new Glob(extensions, { cwd: base, ignore: ignorePattern, fs });
|
|
31
|
+
for await (const file of g) {
|
|
32
|
+
const fullFilepath = path.join(base, file);
|
|
33
|
+
const meta = await stat(fullFilepath);
|
|
34
|
+
if (!meta.isDirectory() && !ig.ignores(file)) {
|
|
35
|
+
matchingFiles.push(fullFilepath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return matchingFiles;
|
|
39
|
+
}
|
|
40
|
+
async function getGitIgnore(base) {
|
|
41
|
+
const gitIgnorePath = path.join(base, ".gitignore");
|
|
42
|
+
try {
|
|
43
|
+
return await readFile(gitIgnorePath, "utf-8");
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function getZeroheightURL() {
|
|
50
|
+
if (process.env["NODE_ENV"] === "dev") {
|
|
51
|
+
return new URL("https://zeroheight.dev");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return new URL("https://zeroheight.com");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Post usage data to zeroheight to store
|
|
59
|
+
*/
|
|
60
|
+
export async function submitUsageData(usage, credentials) {
|
|
61
|
+
const baseURL = getZeroheightURL();
|
|
62
|
+
baseURL.pathname = "/open_api/v1/component_usages";
|
|
63
|
+
if (process.env["NODE_ENV"] === "dev") {
|
|
64
|
+
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
|
|
65
|
+
}
|
|
66
|
+
const response = await fetch(baseURL, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"X-API-CLIENT": credentials.client,
|
|
71
|
+
"X-API-KEY": credentials.token,
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
component_usage: {
|
|
75
|
+
usage: transformUsageByName(usage),
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
if (response.status === 401) {
|
|
80
|
+
throw new Error("Unauthorized");
|
|
81
|
+
}
|
|
82
|
+
return await response.json();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Transform usage map grouped by file into usage map grouped by component
|
|
86
|
+
*/
|
|
87
|
+
function transformUsageByName(usage) {
|
|
88
|
+
const transformedUsageMap = new Map();
|
|
89
|
+
for (const [file, rawUsage] of usage) {
|
|
90
|
+
for (const { count, package: packageName, name } of rawUsage) {
|
|
91
|
+
const key = `package:${packageName} name:${name}`;
|
|
92
|
+
const currentValue = transformedUsageMap.get(key);
|
|
93
|
+
const newFileList = [...(currentValue?.files ?? []), file]; // Add file to list
|
|
94
|
+
transformedUsageMap.set(key, {
|
|
95
|
+
name: currentValue?.name ?? name,
|
|
96
|
+
files: Array.from(new Set(newFileList)), // Ensure unique file paths
|
|
97
|
+
count: count + (currentValue?.count ?? 0),
|
|
98
|
+
package: packageName,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return Array.from(transformedUsageMap.values());
|
|
103
|
+
}
|
|
104
|
+
export async function analyzeFiles(extensions, ignorePattern) {
|
|
105
|
+
const files = await findFiles(process.cwd(), extensions, ignorePattern);
|
|
106
|
+
if (files.length === 0) {
|
|
107
|
+
throw new Error("Can't find any relevant files");
|
|
108
|
+
}
|
|
109
|
+
const parseErrors = [];
|
|
110
|
+
const usageMap = new Map();
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
try {
|
|
113
|
+
const fileContents = await readFile(file, "utf-8");
|
|
114
|
+
const ast = parse(fileContents);
|
|
115
|
+
const usage = analyze(ast);
|
|
116
|
+
if (usage.length > 0) {
|
|
117
|
+
const relativePath = file.slice(process.cwd().length);
|
|
118
|
+
usageMap.set(relativePath, usage);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
parseErrors.push(`Can't parse file ${file}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { errors: parseErrors, usage: usageMap };
|
|
126
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { RenderOptions } from "ink";
|
|
3
|
+
interface AuthOptions {
|
|
4
|
+
client?: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function authAction(options: AuthOptions, renderOptions?: RenderOptions): Promise<void>;
|
|
8
|
+
export declare function authCommand(): Command;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { Box, Text, render } from "ink";
|
|
4
|
+
import CredentialsAlreadyExists from "../components/auth/credentials-already-exists.js";
|
|
5
|
+
import CredentialsPreview from "../components/auth/credentials-preview.js";
|
|
6
|
+
import NoCredentialsOnboarding from "../components/auth/no-credentials-onboarding.js";
|
|
7
|
+
import { configPath, readConfig, writeConfig } from "../common/config.js";
|
|
8
|
+
export async function authAction(options, renderOptions) {
|
|
9
|
+
const existingConfig = await readConfig();
|
|
10
|
+
if (existingConfig) {
|
|
11
|
+
const { waitUntilExit } = render(React.createElement(CredentialsAlreadyExists, { existingConfig: existingConfig, token: options.token, client: options.client, configPath: configPath(), onOverwriteCredentials: async () => {
|
|
12
|
+
const newClient = options.client ?? existingConfig.client;
|
|
13
|
+
const newToken = options.token ?? existingConfig.token;
|
|
14
|
+
await writeConfig(newClient, newToken);
|
|
15
|
+
render(React.createElement(Box, { flexDirection: "column" },
|
|
16
|
+
React.createElement(Text, { color: "green" }, "Updated credentials"),
|
|
17
|
+
React.createElement(CredentialsPreview, { client: newClient, token: newToken })));
|
|
18
|
+
} }), renderOptions);
|
|
19
|
+
await waitUntilExit();
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const { waitUntilExit, rerender } = render(React.createElement(NoCredentialsOnboarding, { configPath: configPath(), onSaveCredentials: async (persist, newClient, newToken) => {
|
|
23
|
+
if (persist) {
|
|
24
|
+
await writeConfig(newClient, newToken);
|
|
25
|
+
rerender(React.createElement(Box, { flexDirection: "column" },
|
|
26
|
+
React.createElement(Text, { color: "green" }, "Updated credentials"),
|
|
27
|
+
React.createElement(CredentialsPreview, { client: newClient, token: newToken })));
|
|
28
|
+
}
|
|
29
|
+
} }));
|
|
30
|
+
await waitUntilExit();
|
|
31
|
+
}
|
|
32
|
+
render(React.createElement(Text, { bold: true }, "Done"));
|
|
33
|
+
}
|
|
34
|
+
export function authCommand() {
|
|
35
|
+
const command = new Command();
|
|
36
|
+
return command
|
|
37
|
+
.command("auth")
|
|
38
|
+
.description("Authenticate with zeroheight")
|
|
39
|
+
.addHelpText("before", "Set credentials for performing actions with zeroheight. Credentials will default to ZEROHEIGHT_CLIENT_ID and ZEROHEIGHT_ACCESS_TOKEN environment variables if not supplied")
|
|
40
|
+
.option("-c, --client <client_id>", "zeroheight Client ID", process.env["ZEROHEIGHT_CLIENT_ID"])
|
|
41
|
+
.option("-t, --token <access_token>", "zeroheight Access Token", process.env["ZEROHEIGHT_ACCESS_TOKEN"])
|
|
42
|
+
.action(async (options) => {
|
|
43
|
+
try {
|
|
44
|
+
await authAction(options);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
console.error(e);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface Credentials {
|
|
2
|
+
token: string;
|
|
3
|
+
client: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function configPath(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Read the credentials from the user's home directory
|
|
8
|
+
* @returns access token and client id or null
|
|
9
|
+
*/
|
|
10
|
+
export declare function readConfig(): Promise<Credentials | null>;
|
|
11
|
+
export declare function writeConfig(client: string, token: string): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
export function configPath() {
|
|
5
|
+
return path.join(homedir(), ".zeroheight-config.json");
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Read the credentials from the user's home directory
|
|
9
|
+
* @returns access token and client id or null
|
|
10
|
+
*/
|
|
11
|
+
export async function readConfig() {
|
|
12
|
+
try {
|
|
13
|
+
const rawData = await readFile(configPath(), "utf8");
|
|
14
|
+
const { token, client } = JSON.parse(rawData);
|
|
15
|
+
return { token, client };
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
const client = process.env["ZEROHEIGHT_CLIENT_ID"];
|
|
19
|
+
const token = process.env["ZEROHEIGHT_ACCESS_TOKEN"];
|
|
20
|
+
if (client && token) {
|
|
21
|
+
return { token, client };
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function writeConfig(client, token) {
|
|
27
|
+
const payload = JSON.stringify({
|
|
28
|
+
client: client,
|
|
29
|
+
token: token,
|
|
30
|
+
});
|
|
31
|
+
return writeFile(configPath(), payload);
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { RawUsageMap } from "../../commands/analyze.js";
|
|
3
|
+
export interface AnalyzeProps {
|
|
4
|
+
onAnalyzeFiles: () => Promise<{
|
|
5
|
+
errors: string[];
|
|
6
|
+
usage: RawUsageMap;
|
|
7
|
+
}>;
|
|
8
|
+
dryRun: boolean;
|
|
9
|
+
}
|
|
10
|
+
export default function Analyze({ onAnalyzeFiles, dryRun }: AnalyzeProps): React.JSX.Element;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Spinner from "ink-spinner";
|
|
3
|
+
import { Box, Newline, Text, useApp } from "ink";
|
|
4
|
+
import { configPath, writeConfig, readConfig, } from "../../common/config.js";
|
|
5
|
+
import UsageTable from "../usage-table.js";
|
|
6
|
+
import ContinuePrompt from "../ui/continue-prompt.js";
|
|
7
|
+
import { getZeroheightURL, submitUsageData, } from "../../commands/analyze.utils.js";
|
|
8
|
+
import Link from "ink-link";
|
|
9
|
+
import NoCredentialsOnboarding from "../auth/no-credentials-onboarding.js";
|
|
10
|
+
export default function Analyze({ onAnalyzeFiles, dryRun }) {
|
|
11
|
+
const { exit } = useApp();
|
|
12
|
+
const [usageResult, setUsageResult] = React.useState(null);
|
|
13
|
+
const [isSendingData, setIsSendingData] = React.useState(false);
|
|
14
|
+
const [errorList, setErrorList] = React.useState([]);
|
|
15
|
+
const [credentials, setCredentials] = React.useState(null);
|
|
16
|
+
const [resourceURL, setResourceURL] = React.useState(null);
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
onAnalyzeFiles()
|
|
19
|
+
.then(({ errors, usage }) => {
|
|
20
|
+
setErrorList((s) => [...s, ...errors]);
|
|
21
|
+
setUsageResult(usage);
|
|
22
|
+
})
|
|
23
|
+
.catch((e) => setErrorList((s) => [...s, e]));
|
|
24
|
+
if (!dryRun) {
|
|
25
|
+
readConfig().then((newCredentials) => setCredentials(newCredentials));
|
|
26
|
+
}
|
|
27
|
+
}, []);
|
|
28
|
+
async function handleContinue(shouldContinue) {
|
|
29
|
+
if (shouldContinue) {
|
|
30
|
+
if (!dryRun && credentials && usageResult) {
|
|
31
|
+
setIsSendingData(true);
|
|
32
|
+
try {
|
|
33
|
+
await submitUsageData(usageResult, credentials);
|
|
34
|
+
const resourceURL = getZeroheightURL();
|
|
35
|
+
resourceURL.pathname = "/adoption/";
|
|
36
|
+
setResourceURL(resourceURL);
|
|
37
|
+
}
|
|
38
|
+
catch (_e) {
|
|
39
|
+
setErrorList((s) => [...s, "Failed to send data to zeroheight"]);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
setIsSendingData(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
exit();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (errorList.length > 0) {
|
|
51
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
52
|
+
React.createElement(Text, { color: "red" }, "Error:"),
|
|
53
|
+
React.createElement(Box, { flexDirection: "column", marginLeft: 1 }, errorList.map((e) => (React.createElement(Text, { key: e },
|
|
54
|
+
"- ",
|
|
55
|
+
e))))));
|
|
56
|
+
}
|
|
57
|
+
if (!credentials && !dryRun) {
|
|
58
|
+
return (React.createElement(NoCredentialsOnboarding, { configPath: configPath(), onSaveCredentials: async (persist, newClient, newToken) => {
|
|
59
|
+
const newCredentials = { token: newToken, client: newClient };
|
|
60
|
+
setCredentials(newCredentials);
|
|
61
|
+
if (persist) {
|
|
62
|
+
await writeConfig(newClient, newToken);
|
|
63
|
+
}
|
|
64
|
+
} }));
|
|
65
|
+
}
|
|
66
|
+
// Usage is being calculated
|
|
67
|
+
if (usageResult === null) {
|
|
68
|
+
return (React.createElement(Text, null,
|
|
69
|
+
React.createElement(Text, { color: "green" },
|
|
70
|
+
React.createElement(Spinner, { type: "dots" })),
|
|
71
|
+
" Scanning files..."));
|
|
72
|
+
}
|
|
73
|
+
// No usage data found
|
|
74
|
+
if (usageResult?.size === 0) {
|
|
75
|
+
return (React.createElement(Text, null, "No files found, try a different directory or change the file extensions you want to include"));
|
|
76
|
+
}
|
|
77
|
+
// Posting usage data to zeroheight
|
|
78
|
+
if (isSendingData) {
|
|
79
|
+
return (React.createElement(Text, null,
|
|
80
|
+
React.createElement(Text, { color: "green" },
|
|
81
|
+
React.createElement(Spinner, { type: "dots" })),
|
|
82
|
+
" Sending usage data to zeroheight..."));
|
|
83
|
+
}
|
|
84
|
+
// Completed, usage data is ready to view on zeroheight
|
|
85
|
+
if (resourceURL) {
|
|
86
|
+
return (React.createElement(React.Fragment, null,
|
|
87
|
+
React.createElement(Newline, null),
|
|
88
|
+
React.createElement(Text, null,
|
|
89
|
+
React.createElement(Text, { color: "green" }, "Successful:"),
|
|
90
|
+
" View your",
|
|
91
|
+
" ",
|
|
92
|
+
React.createElement(Link, { url: resourceURL.toString() }, "usage data on zeroheight"))));
|
|
93
|
+
}
|
|
94
|
+
if (usageResult) {
|
|
95
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
96
|
+
React.createElement(UsageTable, { usage: usageResult }),
|
|
97
|
+
dryRun && (React.createElement(React.Fragment, null,
|
|
98
|
+
React.createElement(Newline, null),
|
|
99
|
+
React.createElement(Text, null,
|
|
100
|
+
"Nothing sent to zeroheight, if you want to track component usage data then remove the ",
|
|
101
|
+
React.createElement(Text, { italic: true }, "--dry-run"),
|
|
102
|
+
" option."),
|
|
103
|
+
React.createElement(Newline, null))),
|
|
104
|
+
!dryRun && React.createElement(ContinuePrompt, { onContinue: handleContinue })));
|
|
105
|
+
}
|
|
106
|
+
return React.createElement(Text, null, "NOPE");
|
|
107
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
interface CredentialsAlreadyExistsProps {
|
|
3
|
+
client?: string;
|
|
4
|
+
token?: string;
|
|
5
|
+
configPath: string;
|
|
6
|
+
existingConfig: {
|
|
7
|
+
client: string;
|
|
8
|
+
token: string;
|
|
9
|
+
};
|
|
10
|
+
onOverwriteCredentials: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Show when credentials already exist and prompt user to overwrite if applicable
|
|
14
|
+
*/
|
|
15
|
+
export default function CredentialsAlreadyExists({ client, token, configPath, existingConfig, onOverwriteCredentials, }: CredentialsAlreadyExistsProps): React.JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Box, Text, useApp } from "ink";
|
|
3
|
+
import Link from "ink-link";
|
|
4
|
+
import CredentialsPreview from "./credentials-preview.js";
|
|
5
|
+
import ConfirmInput from "../ui/confirm-input.js";
|
|
6
|
+
/**
|
|
7
|
+
* Show when credentials already exist and prompt user to overwrite if applicable
|
|
8
|
+
*/
|
|
9
|
+
export default function CredentialsAlreadyExists({ client, token, configPath, existingConfig, onOverwriteCredentials, }) {
|
|
10
|
+
const { exit } = useApp();
|
|
11
|
+
const [shouldOverwrite, setShouldOverwrite] = React.useState("");
|
|
12
|
+
async function handleSubmitOverwrite(override) {
|
|
13
|
+
if (override) {
|
|
14
|
+
await onOverwriteCredentials();
|
|
15
|
+
}
|
|
16
|
+
exit();
|
|
17
|
+
}
|
|
18
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
19
|
+
React.createElement(Text, { bold: true, color: "yellow" }, "Credentials already exist"),
|
|
20
|
+
React.createElement(Text, null,
|
|
21
|
+
"You already have credentials in",
|
|
22
|
+
" ",
|
|
23
|
+
React.createElement(Link, { url: configPath }, configPath)),
|
|
24
|
+
React.createElement(CredentialsPreview, { client: existingConfig.client, token: existingConfig.token }),
|
|
25
|
+
(client || token) && (React.createElement(Box, null,
|
|
26
|
+
React.createElement(Text, null,
|
|
27
|
+
"Would you like to overwrite these credentials?",
|
|
28
|
+
" ",
|
|
29
|
+
React.createElement(Text, { dimColor: true }, "(y/N)")),
|
|
30
|
+
React.createElement(ConfirmInput, { isChecked: false, value: shouldOverwrite, onChange: setShouldOverwrite, onSubmit: handleSubmitOverwrite })))));
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
interface CredentialsPreviewProps {
|
|
3
|
+
client: string;
|
|
4
|
+
token: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Show Client ID and Access Token (redacted/sensitive) for confirmation
|
|
8
|
+
*/
|
|
9
|
+
export default function CredentialsPreview({ client, token, }: CredentialsPreviewProps): React.JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
const REDACT_REVEAL_COUNT = 6;
|
|
4
|
+
/**
|
|
5
|
+
* Show Client ID and Access Token (redacted/sensitive) for confirmation
|
|
6
|
+
*/
|
|
7
|
+
export default function CredentialsPreview({ client, token, }) {
|
|
8
|
+
const asterisks = new Array(Math.max(3, token.length - 2 * REDACT_REVEAL_COUNT)).fill("✲");
|
|
9
|
+
return (React.createElement(Box, { borderStyle: "single", margin: 1, padding: 1 },
|
|
10
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
11
|
+
React.createElement(Box, { gap: 7 },
|
|
12
|
+
React.createElement(Text, { bold: true }, "Client ID:"),
|
|
13
|
+
React.createElement(Text, null, client)),
|
|
14
|
+
React.createElement(Box, { gap: 4 },
|
|
15
|
+
React.createElement(Text, { bold: true }, "Access Token:"),
|
|
16
|
+
React.createElement(Text, null,
|
|
17
|
+
token.slice(0, REDACT_REVEAL_COUNT),
|
|
18
|
+
asterisks,
|
|
19
|
+
token.slice(token.length - REDACT_REVEAL_COUNT, token.length))))));
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
interface NoCredentialsOnboardingProps {
|
|
3
|
+
onSaveCredentials: (storeCredentials: boolean, client: string, token: string) => Promise<void>;
|
|
4
|
+
configPath: string;
|
|
5
|
+
}
|
|
6
|
+
export default function NoCredentialsOnboarding({ onSaveCredentials, configPath, }: NoCredentialsOnboardingProps): React.JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Box, Newline, Text } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import Link from "ink-link";
|
|
5
|
+
import CredentialsPreview from "./credentials-preview.js";
|
|
6
|
+
import ConfirmInput from "../ui/confirm-input.js";
|
|
7
|
+
export default function NoCredentialsOnboarding({ onSaveCredentials, configPath, }) {
|
|
8
|
+
const [client, setClient] = React.useState("");
|
|
9
|
+
const [token, setToken] = React.useState("");
|
|
10
|
+
const [shouldWrite, setShouldWrite] = React.useState("");
|
|
11
|
+
const [field, setField] = React.useState("client");
|
|
12
|
+
const handleWriteCredentials = async (answer) => {
|
|
13
|
+
await onSaveCredentials(answer, client.trim(), token.trim());
|
|
14
|
+
};
|
|
15
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
16
|
+
React.createElement(Text, null,
|
|
17
|
+
"If you need a new auth token, generate them from the",
|
|
18
|
+
" ",
|
|
19
|
+
React.createElement(Link, { url: "https://zeroheight.com/settings/team/developers" }, "Developer Settings")),
|
|
20
|
+
field === "client" && (React.createElement(Box, null,
|
|
21
|
+
React.createElement(Text, null, "Client ID: "),
|
|
22
|
+
React.createElement(TextInput, { onChange: setClient, value: client, onSubmit: (v) => {
|
|
23
|
+
if (v.trim().length === 0)
|
|
24
|
+
return;
|
|
25
|
+
setField("token");
|
|
26
|
+
} }))),
|
|
27
|
+
field === "token" && (React.createElement(Box, null,
|
|
28
|
+
React.createElement(Text, null, "Access Token:"),
|
|
29
|
+
React.createElement(TextInput, { onChange: setToken, value: token, onSubmit: (v) => {
|
|
30
|
+
if (v.trim().length === 0)
|
|
31
|
+
return;
|
|
32
|
+
setField("write");
|
|
33
|
+
} }))),
|
|
34
|
+
field === "write" && (React.createElement(Box, { flexDirection: "column" },
|
|
35
|
+
React.createElement(CredentialsPreview, { client: client, token: token }),
|
|
36
|
+
React.createElement(Text, null,
|
|
37
|
+
"Would you like to write these credentials to",
|
|
38
|
+
" ",
|
|
39
|
+
React.createElement(Link, { url: configPath }, configPath),
|
|
40
|
+
"?",
|
|
41
|
+
" ",
|
|
42
|
+
React.createElement(Text, { dimColor: true }, "(Y/n)")),
|
|
43
|
+
React.createElement(ConfirmInput, { isChecked: true, onChange: setShouldWrite, onSubmit: handleWriteCredentials, value: shouldWrite }))),
|
|
44
|
+
React.createElement(Newline, null)));
|
|
45
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Box, Newline, Text } from "ink";
|
|
3
|
+
import Link from "ink-link";
|
|
4
|
+
/**
|
|
5
|
+
* Rich help banner with link to help center docs
|
|
6
|
+
*/
|
|
7
|
+
export default function HelpInfo() {
|
|
8
|
+
return (React.createElement(React.Fragment, null,
|
|
9
|
+
React.createElement(Box, { borderStyle: "double", padding: 2, margin: 1, flexDirection: "column" },
|
|
10
|
+
React.createElement(Box, { justifyContent: "center" },
|
|
11
|
+
React.createElement(Text, { bold: true }, "Welcome \uD83D\uDC4B")),
|
|
12
|
+
React.createElement(Newline, null),
|
|
13
|
+
React.createElement(Text, null,
|
|
14
|
+
"Get started with the",
|
|
15
|
+
" ",
|
|
16
|
+
React.createElement(Text, { color: "#f63e7c", bold: true }, "zeroheight"),
|
|
17
|
+
" ",
|
|
18
|
+
"measurement CLI and start tracking your component usage."),
|
|
19
|
+
React.createElement(Text, null,
|
|
20
|
+
"For more information on how to set this up, check",
|
|
21
|
+
" ",
|
|
22
|
+
React.createElement(Link, { url: "https://zeroheight.com" }, "here"),
|
|
23
|
+
"."))));
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
interface InkConfirmInputProps {
|
|
3
|
+
isChecked: boolean;
|
|
4
|
+
onChange: (newValue: string) => void;
|
|
5
|
+
onSubmit: (value: boolean) => void;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}
|
|
9
|
+
declare const ConfirmInput: ({ isChecked, onChange, onSubmit, placeholder, value, ...props }: InkConfirmInputProps) => React.JSX.Element;
|
|
10
|
+
export default ConfirmInput;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import TextInput from "ink-text-input";
|
|
3
|
+
import yn from "yn";
|
|
4
|
+
const ConfirmInput = ({ isChecked, onChange, onSubmit, placeholder, value, ...props }) => {
|
|
5
|
+
const handleSubmit = React.useCallback((newValue) => {
|
|
6
|
+
onSubmit(yn(newValue, { default: isChecked }));
|
|
7
|
+
}, [isChecked, onSubmit]);
|
|
8
|
+
return (React.createElement(TextInput, { ...props, placeholder: placeholder, value: value, onChange: onChange, onSubmit: handleSubmit }));
|
|
9
|
+
};
|
|
10
|
+
export default ConfirmInput;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import ConfirmInput from "./confirm-input.js";
|
|
4
|
+
export default function ContinuePrompt({ onContinue }) {
|
|
5
|
+
const [shouldContinueText, setShouldContinueText] = React.useState("");
|
|
6
|
+
return (React.createElement(Box, null,
|
|
7
|
+
React.createElement(Text, null,
|
|
8
|
+
"Continue? ",
|
|
9
|
+
React.createElement(Text, { dimColor: true }, "(Y/n):")),
|
|
10
|
+
React.createElement(ConfirmInput, { isChecked: true, value: shouldContinueText, onChange: setShouldContinueText, onSubmit: (answer) => {
|
|
11
|
+
onContinue(answer);
|
|
12
|
+
} })));
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
export default function UsageTable({ usage }) {
|
|
4
|
+
const rows = Array.from(usage.entries());
|
|
5
|
+
return (React.createElement(Box, { flexDirection: "column" }, rows.map(([filepath, fileUsage]) => {
|
|
6
|
+
return (React.createElement(Box, { key: filepath, flexDirection: "column" },
|
|
7
|
+
React.createElement(Text, { color: "green" }, filepath),
|
|
8
|
+
React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, fileUsage.map(({ name, count }) => {
|
|
9
|
+
return (React.createElement(Box, { gap: 2, key: `${name}-${filepath}` },
|
|
10
|
+
React.createElement(Text, { bold: true }, count),
|
|
11
|
+
React.createElement(Text, null, name)));
|
|
12
|
+
}))));
|
|
13
|
+
})));
|
|
14
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zeroheight/adoption-cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"license": "ISC",
|
|
5
|
+
"main": "dist/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zh-adoption": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=16"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"cleanup": "rm -rf dist",
|
|
15
|
+
"setup": "npm run build && npm i -g",
|
|
16
|
+
"start": "zh-adoption",
|
|
17
|
+
"start-dev": "export NODE_ENV=dev && zh-adoption",
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"lint": "prettier --check .",
|
|
21
|
+
"test": "vitest"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"commander": "^12.0.0",
|
|
28
|
+
"ignore": "^5.3.1",
|
|
29
|
+
"ink": "^4.1.0",
|
|
30
|
+
"react": "^18.2.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@sindresorhus/tsconfig": "^3.0.1",
|
|
34
|
+
"@types/react": "^18.0.32",
|
|
35
|
+
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
|
36
|
+
"@typescript-eslint/parser": "^7.7.1",
|
|
37
|
+
"@vdemedes/prettier-config": "^2.0.1",
|
|
38
|
+
"acorn": "^8.11.3",
|
|
39
|
+
"acorn-jsx-walk": "^2.0.0",
|
|
40
|
+
"acorn-typescript": "^1.4.13",
|
|
41
|
+
"acorn-walk": "^8.3.2",
|
|
42
|
+
"chalk": "^5.2.0",
|
|
43
|
+
"eslint-plugin-react": "^7.32.2",
|
|
44
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
45
|
+
"glob": "^10.3.15",
|
|
46
|
+
"ink-link": "^3.0.0",
|
|
47
|
+
"ink-render-string": "^1.0.0",
|
|
48
|
+
"ink-spinner": "^5.0.0",
|
|
49
|
+
"ink-testing-library": "git+ssh://git@github.com/vadimdemedes/ink-testing-library.git#f44b077e9a05a1d615bab41c72906726d34ea085",
|
|
50
|
+
"ink-text-input": "^5.0.1",
|
|
51
|
+
"memfs": "^4.9.2",
|
|
52
|
+
"mock-stdin": "^1.0.0",
|
|
53
|
+
"prettier": "^2.8.7",
|
|
54
|
+
"ts-node": "^10.9.1",
|
|
55
|
+
"typescript": "^5.4.5",
|
|
56
|
+
"vitest": "^1.5.2",
|
|
57
|
+
"yn": "^5.0.0"
|
|
58
|
+
},
|
|
59
|
+
"types": "./dist/cli.d.ts",
|
|
60
|
+
"description": "CLI for measuring component usage",
|
|
61
|
+
"repository": {
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "git+https://github.com/zeroheight/zh-measure-cli.git"
|
|
64
|
+
},
|
|
65
|
+
"author": "zeroheight",
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/zeroheight/zh-measure-cli/issues"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/zeroheight/zh-measure-cli#readme"
|
|
70
|
+
}
|