@zeroheight/adoption-cli 0.3.1 → 0.4.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/CHANGELOG.md +4 -0
- package/dist/cli.js +1 -1
- package/dist/commands/analyze.d.ts +1 -0
- package/dist/commands/analyze.js +12 -2
- package/dist/commands/analyze.utils.d.ts +1 -0
- package/dist/commands/analyze.utils.js +17 -2
- package/dist/components/analyze/analyze.js +55 -51
- package/dist/components/analyze/non-interactive-analyze.d.ts +10 -0
- package/dist/components/analyze/non-interactive-analyze.js +124 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/0.3.1) - 2nd August 2024
|
|
4
|
+
|
|
5
|
+
- Allow `--interactive false` option for `analyze` command to better sipport CI
|
|
6
|
+
|
|
3
7
|
## [0.3.1](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/0.3.1) - 30th July 2024
|
|
4
8
|
|
|
5
9
|
- Condense analyze output to be easier to parse
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ const { output, cleanup } = render(React.createElement(HelpInfo, null));
|
|
|
10
10
|
program
|
|
11
11
|
.name("zh-adoption")
|
|
12
12
|
.description("CLI for measuring design system usage usage in your products")
|
|
13
|
-
.version("0.
|
|
13
|
+
.version("0.4.0")
|
|
14
14
|
.addHelpText("before", output)
|
|
15
15
|
.addCommand(analyzeCommand())
|
|
16
16
|
.addCommand(authCommand());
|
|
@@ -6,6 +6,7 @@ interface AnalyzeOptions {
|
|
|
6
6
|
ignore: string;
|
|
7
7
|
dryRun: boolean;
|
|
8
8
|
repoName?: string;
|
|
9
|
+
interactive: boolean;
|
|
9
10
|
}
|
|
10
11
|
export type RawUsageMap = Map<string, RawUsage[]>;
|
|
11
12
|
export declare function analyzeAction(options: AnalyzeOptions, renderOptions?: RenderOptions): Promise<void>;
|
package/dist/commands/analyze.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { render } from "ink";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
|
-
import
|
|
4
|
+
import yn from "yn";
|
|
5
5
|
import { analyzeFiles } from "./analyze.utils.js";
|
|
6
|
+
import Analyze from "../components/analyze/analyze.js";
|
|
7
|
+
import NonInteractiveAnalyze from "../components/analyze/non-interactive-analyze.js";
|
|
6
8
|
export async function analyzeAction(options, renderOptions) {
|
|
7
|
-
|
|
9
|
+
if (options.interactive) {
|
|
10
|
+
render(React.createElement(Analyze, { dryRun: options.dryRun, onAnalyzeFiles: () => analyzeFiles(options.extensions, options.ignore), repoName: options.repoName }), renderOptions);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
render(React.createElement(NonInteractiveAnalyze, { repoName: options.repoName, onAnalyzeFiles: () => analyzeFiles(options.extensions, options.ignore) }));
|
|
14
|
+
}
|
|
8
15
|
}
|
|
9
16
|
export function analyzeCommand() {
|
|
10
17
|
const command = new Command();
|
|
@@ -15,6 +22,9 @@ export function analyzeCommand() {
|
|
|
15
22
|
.addOption(new Option("-i, --ignore [ext]", "files to ignore when searching for components").default("**/*.{test,spec}.*", "glob pattern to determine ignored files"))
|
|
16
23
|
.addOption(new Option("-d, --dry-run", "don't push results to zeroheight").default(false))
|
|
17
24
|
.addOption(new Option("-r, --repo-name <string>", "name of the repository"))
|
|
25
|
+
.addOption(new Option("-in, --interactive [boolean]", "disable to skip input (useful when running in CI)")
|
|
26
|
+
.default(true)
|
|
27
|
+
.argParser((value) => yn(value)))
|
|
18
28
|
.action(async (options) => {
|
|
19
29
|
try {
|
|
20
30
|
await analyzeAction(options);
|
|
@@ -70,8 +70,23 @@ export async function analyzeFiles(extensions, ignorePattern) {
|
|
|
70
70
|
const errorFile = `/tmp/zh-adoption-analyze-errors-${Date.now()}`;
|
|
71
71
|
if (parseErrors.length > 0) {
|
|
72
72
|
const file = fs.createWriteStream(errorFile);
|
|
73
|
-
parseErrors.forEach((err) => {
|
|
73
|
+
parseErrors.forEach((err) => {
|
|
74
|
+
file.write(err + "\n");
|
|
75
|
+
});
|
|
74
76
|
file.end();
|
|
75
77
|
}
|
|
76
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
errorFile: parseErrors.length > 0 ? errorFile : null,
|
|
80
|
+
usage: usageMap,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function calculateNumberOfComponents(usageResult) {
|
|
84
|
+
if (usageResult) {
|
|
85
|
+
const components = Array.from(usageResult.entries()).flatMap((comps) => comps[1]);
|
|
86
|
+
const componentCount = components.reduce((prev, next) => {
|
|
87
|
+
return prev + next.count;
|
|
88
|
+
}, 0);
|
|
89
|
+
return componentCount;
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
77
92
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import Spinner from "ink-spinner";
|
|
3
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
4
|
import Link from "ink-link";
|
|
5
|
+
import ConfirmInput from "../ui/confirm-input.js";
|
|
6
|
+
import ContinuePrompt from "../ui/continue-prompt.js";
|
|
8
7
|
import NoCredentialsOnboarding from "../auth/no-credentials-onboarding.js";
|
|
9
|
-
import { getZeroheightURL, submitUsageData } from "../../common/api.js";
|
|
10
8
|
import RepoNamePrompt from "../repo-name-prompt.js";
|
|
11
|
-
import
|
|
9
|
+
import UsageTable from "../usage-table.js";
|
|
10
|
+
import { configPath, writeConfig, readConfig, } from "../../common/config.js";
|
|
11
|
+
import { getZeroheightURL, submitUsageData } from "../../common/api.js";
|
|
12
|
+
import { calculateNumberOfComponents } from "../../commands/analyze.utils.js";
|
|
12
13
|
export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
|
|
13
14
|
const { exit } = useApp();
|
|
14
15
|
const [usageResult, setUsageResult] = React.useState(null);
|
|
@@ -22,20 +23,46 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
|
|
|
22
23
|
const [showContinue, setShowContinue] = React.useState(false);
|
|
23
24
|
const [showMoreResults, setShowMoreResults] = React.useState("");
|
|
24
25
|
React.useEffect(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
(async () => {
|
|
27
|
+
try {
|
|
28
|
+
const { errorFile, usage } = await onAnalyzeFiles();
|
|
29
|
+
setErrorFileLocation(errorFile);
|
|
30
|
+
setUsageResult(usage);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
setErrorList((s) => [...s, e]);
|
|
34
|
+
}
|
|
35
|
+
if (dryRun)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
const config = await readConfig();
|
|
33
39
|
if (config) {
|
|
34
40
|
setCredentials({ token: config.token, client: config.client });
|
|
35
41
|
}
|
|
36
|
-
}
|
|
37
|
-
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
setErrorList((s) => [...s, "Failed to load credentials"]);
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
38
47
|
}, []);
|
|
48
|
+
async function sendData(result, repoName, credentials) {
|
|
49
|
+
setIsSendingData(true);
|
|
50
|
+
try {
|
|
51
|
+
await submitUsageData(result, repoName, credentials);
|
|
52
|
+
const resourceURL = getZeroheightURL();
|
|
53
|
+
resourceURL.pathname = "/adoption/";
|
|
54
|
+
setResourceURL(resourceURL);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
const errorMessage = e.message === "Unauthorized"
|
|
58
|
+
? "Unauthorized. Please reset your authentication by running: zh-adoption auth"
|
|
59
|
+
: "Failed to send data to zeroheight";
|
|
60
|
+
setErrorList((s) => [...s, errorMessage]);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
setIsSendingData(false);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
39
66
|
async function handleContinue(shouldContinue) {
|
|
40
67
|
if (shouldContinue) {
|
|
41
68
|
if (!dryRun && credentials && usageResult && !repo) {
|
|
@@ -43,22 +70,7 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
|
|
|
43
70
|
setRepo("");
|
|
44
71
|
}
|
|
45
72
|
else if (!dryRun && credentials && usageResult && repo) {
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
await submitUsageData(usageResult, repo, credentials);
|
|
49
|
-
const resourceURL = getZeroheightURL();
|
|
50
|
-
resourceURL.pathname = "/adoption/";
|
|
51
|
-
setResourceURL(resourceURL);
|
|
52
|
-
}
|
|
53
|
-
catch (e) {
|
|
54
|
-
const errorMessage = e.message === "Unauthorized"
|
|
55
|
-
? "Unauthorized. Please reset your authentication by running: zh-adoption auth"
|
|
56
|
-
: "Failed to send data to zeroheight";
|
|
57
|
-
setErrorList((s) => [...s, errorMessage]);
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
setIsSendingData(false);
|
|
61
|
-
}
|
|
73
|
+
sendData(usageResult, repo, credentials);
|
|
62
74
|
}
|
|
63
75
|
}
|
|
64
76
|
else {
|
|
@@ -76,22 +88,12 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
|
|
|
76
88
|
}
|
|
77
89
|
function handleShowMore(showMore) {
|
|
78
90
|
if (showMore) {
|
|
79
|
-
setShowMoreResults(
|
|
91
|
+
setShowMoreResults("true");
|
|
80
92
|
}
|
|
81
93
|
else {
|
|
82
94
|
setShowContinue(true);
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
|
-
function calculateNumberOfComponents() {
|
|
86
|
-
if (usageResult) {
|
|
87
|
-
const components = Array.from(usageResult.entries()).flatMap((comps) => comps[1]);
|
|
88
|
-
const componentCount = components.reduce((prev, next) => {
|
|
89
|
-
return prev + next.count;
|
|
90
|
-
}, 0);
|
|
91
|
-
return componentCount;
|
|
92
|
-
}
|
|
93
|
-
return 0;
|
|
94
|
-
}
|
|
95
97
|
if (errorList.length > 0) {
|
|
96
98
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
97
99
|
React.createElement(Text, { color: "red" }, "Error:"),
|
|
@@ -99,7 +101,7 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
|
|
|
99
101
|
"- ",
|
|
100
102
|
e))))));
|
|
101
103
|
}
|
|
102
|
-
if (!credentials && !dryRun) {
|
|
104
|
+
if (!promptRepo && !credentials && !dryRun) {
|
|
103
105
|
return (React.createElement(NoCredentialsOnboarding, { configPath: configPath(), onSaveCredentials: async (persist, newClient, newToken) => {
|
|
104
106
|
const newCredentials = { token: newToken, client: newClient };
|
|
105
107
|
setCredentials(newCredentials);
|
|
@@ -139,27 +141,29 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
|
|
|
139
141
|
" ",
|
|
140
142
|
React.createElement(Link, { url: resourceURL.toString() }, "usage data on zeroheight"))));
|
|
141
143
|
}
|
|
142
|
-
if (usageResult &&
|
|
144
|
+
if (usageResult &&
|
|
145
|
+
!promptRepo &&
|
|
146
|
+
!(showMoreResults === "true" || showContinue)) {
|
|
143
147
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
144
148
|
React.createElement(Text, { color: "green" },
|
|
145
|
-
calculateNumberOfComponents(),
|
|
149
|
+
calculateNumberOfComponents(usageResult),
|
|
146
150
|
" components found"),
|
|
147
151
|
errorFileLocation && (React.createElement(React.Fragment, null,
|
|
148
152
|
React.createElement(Newline, null),
|
|
149
153
|
React.createElement(Box, null,
|
|
150
154
|
React.createElement(Text, { color: "red" }, "Error:"),
|
|
151
155
|
React.createElement(Text, null,
|
|
152
|
-
|
|
153
|
-
"Some files could not be parsed. Errors can be found at
|
|
156
|
+
" ",
|
|
157
|
+
"Some files could not be parsed. Errors can be found at",
|
|
158
|
+
" ",
|
|
154
159
|
React.createElement(Link, { url: errorFileLocation }, errorFileLocation))))),
|
|
155
160
|
React.createElement(Box, null,
|
|
156
161
|
React.createElement(Text, null,
|
|
157
|
-
"Show more details?",
|
|
158
|
-
" ",
|
|
162
|
+
"Show more details? ",
|
|
159
163
|
React.createElement(Text, { dimColor: true }, "(y/N)")),
|
|
160
164
|
React.createElement(ConfirmInput, { isChecked: false, value: showMoreResults, onChange: setShowMoreResults, onSubmit: handleShowMore }))));
|
|
161
165
|
}
|
|
162
|
-
if (usageResult && !promptRepo && showMoreResults ===
|
|
166
|
+
if (usageResult && !promptRepo && showMoreResults === "true") {
|
|
163
167
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
164
168
|
React.createElement(UsageTable, { usage: usageResult }),
|
|
165
169
|
dryRun && (React.createElement(React.Fragment, null,
|
|
@@ -185,5 +189,5 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
|
|
|
185
189
|
if (promptRepo) {
|
|
186
190
|
return (React.createElement(RepoNamePrompt, { credentials: credentials, onRepoNameSelected: handleOnRepoNameSelected }));
|
|
187
191
|
}
|
|
188
|
-
return React.createElement(Text, null, "
|
|
192
|
+
return React.createElement(Text, null, "Done");
|
|
189
193
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { RawUsageMap } from "../../commands/analyze.js";
|
|
3
|
+
export interface NonInteractiveAnalyzeProps {
|
|
4
|
+
onAnalyzeFiles: () => Promise<{
|
|
5
|
+
errorFile: string | null;
|
|
6
|
+
usage: RawUsageMap;
|
|
7
|
+
}>;
|
|
8
|
+
repoName?: string;
|
|
9
|
+
}
|
|
10
|
+
export default function NonInteractiveAnalyze({ repoName, onAnalyzeFiles, }: NonInteractiveAnalyzeProps): React.JSX.Element;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Newline, Text } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import Link from "ink-link";
|
|
5
|
+
import { calculateNumberOfComponents } from "../../commands/analyze.utils.js";
|
|
6
|
+
import { readConfig } from "../../common/config.js";
|
|
7
|
+
import { submitUsageData } from "../../common/api.js";
|
|
8
|
+
export default function NonInteractiveAnalyze({ repoName, onAnalyzeFiles, }) {
|
|
9
|
+
const [isSendingData, setIsSendingData] = React.useState(false);
|
|
10
|
+
const [isAnalyzingFiles, setIsAnalyzingFiles] = React.useState(false);
|
|
11
|
+
const [isFetchingCredentials, setIsFetchingCredentials] = React.useState(false);
|
|
12
|
+
const [errorFileLocation, setErrorFileLocation] = React.useState(null);
|
|
13
|
+
const [errorList, setErrorList] = React.useState([]);
|
|
14
|
+
const [credentials, setCredentials] = React.useState(null);
|
|
15
|
+
const [usage, setUsage] = React.useState(null);
|
|
16
|
+
async function loadCredentials() {
|
|
17
|
+
setIsFetchingCredentials(true);
|
|
18
|
+
try {
|
|
19
|
+
const config = await readConfig();
|
|
20
|
+
if (config) {
|
|
21
|
+
setCredentials({ token: config.token, client: config.client });
|
|
22
|
+
}
|
|
23
|
+
return config;
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
setErrorList((s) => [...s, "Failed to load credentials"]);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
setIsFetchingCredentials(false);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
async function sendData(result, repoName, credentials) {
|
|
34
|
+
setIsSendingData(true);
|
|
35
|
+
try {
|
|
36
|
+
await submitUsageData(result, repoName, credentials);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
const errorMessage = e.message === "Unauthorized"
|
|
40
|
+
? "Unauthorized. Please reset your authentication by running: zh-adoption auth"
|
|
41
|
+
: "Failed to send data to zeroheight";
|
|
42
|
+
setErrorList((s) => [...s, errorMessage]);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
setIsSendingData(false);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function anaylzeUsage() {
|
|
49
|
+
setIsAnalyzingFiles(true);
|
|
50
|
+
const { errorFile, usage } = await onAnalyzeFiles();
|
|
51
|
+
setUsage(usage);
|
|
52
|
+
if (errorFile) {
|
|
53
|
+
setErrorFileLocation(errorFile);
|
|
54
|
+
}
|
|
55
|
+
setIsAnalyzingFiles(false);
|
|
56
|
+
return usage;
|
|
57
|
+
}
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
(async () => {
|
|
60
|
+
const credentials = await loadCredentials();
|
|
61
|
+
if (!credentials)
|
|
62
|
+
return;
|
|
63
|
+
const usage = await anaylzeUsage();
|
|
64
|
+
if (!repoName)
|
|
65
|
+
return;
|
|
66
|
+
await sendData(usage, repoName, credentials);
|
|
67
|
+
})();
|
|
68
|
+
}, []);
|
|
69
|
+
if (isAnalyzingFiles || isFetchingCredentials || isSendingData) {
|
|
70
|
+
return (React.createElement(Text, null,
|
|
71
|
+
React.createElement(Spinner, { type: "dots" }),
|
|
72
|
+
isAnalyzingFiles && React.createElement(Text, null, "Analyzing components"),
|
|
73
|
+
isFetchingCredentials && React.createElement(Text, null, "Loading credentials"),
|
|
74
|
+
isSendingData && React.createElement(Text, null, "Sending data to zerohegiht")));
|
|
75
|
+
}
|
|
76
|
+
if (errorList.length > 0) {
|
|
77
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
78
|
+
React.createElement(Text, { color: "red" }, "Error:"),
|
|
79
|
+
React.createElement(Box, { flexDirection: "column", marginLeft: 1 }, errorList.map((e) => (React.createElement(Text, { key: e },
|
|
80
|
+
"- ",
|
|
81
|
+
e))))));
|
|
82
|
+
}
|
|
83
|
+
if (errorFileLocation) {
|
|
84
|
+
return (React.createElement(React.Fragment, null,
|
|
85
|
+
React.createElement(Newline, null),
|
|
86
|
+
React.createElement(Box, null,
|
|
87
|
+
React.createElement(Text, { color: "red" }, "Error:"),
|
|
88
|
+
React.createElement(Text, null,
|
|
89
|
+
" ",
|
|
90
|
+
"Some files could not be parsed. Errors can be found at",
|
|
91
|
+
" ",
|
|
92
|
+
React.createElement(Link, { url: errorFileLocation }, errorFileLocation)))));
|
|
93
|
+
}
|
|
94
|
+
if (!repoName) {
|
|
95
|
+
return (React.createElement(Text, null,
|
|
96
|
+
React.createElement(Text, { bold: true }, "Repo name"),
|
|
97
|
+
" not found, run the ",
|
|
98
|
+
React.createElement(Text, { bold: true }, "analyze"),
|
|
99
|
+
" ",
|
|
100
|
+
"command in interactive mode or provide the",
|
|
101
|
+
" ",
|
|
102
|
+
React.createElement(Text, { italic: true },
|
|
103
|
+
"--repo-name ",
|
|
104
|
+
"<my repo name>"),
|
|
105
|
+
" option"));
|
|
106
|
+
}
|
|
107
|
+
if (!credentials) {
|
|
108
|
+
return (React.createElement(Text, null,
|
|
109
|
+
"Credentials not found, please ensure they are set before running the",
|
|
110
|
+
" ",
|
|
111
|
+
React.createElement(Text, { bold: true }, "analyze"),
|
|
112
|
+
" command with",
|
|
113
|
+
" ",
|
|
114
|
+
React.createElement(Text, { italic: true }, "--interactive false")));
|
|
115
|
+
}
|
|
116
|
+
if (!usage) {
|
|
117
|
+
return React.createElement(Text, null, "No usage could be found.");
|
|
118
|
+
}
|
|
119
|
+
return (React.createElement(React.Fragment, null, usage && (React.createElement(Text, { color: "green" },
|
|
120
|
+
calculateNumberOfComponents(usage),
|
|
121
|
+
" components found and sent to",
|
|
122
|
+
" ",
|
|
123
|
+
React.createElement(Text, { color: "#f63e7c" }, "zeroheight")))));
|
|
124
|
+
}
|