@zeroheight/adoption-cli 2.4.3 → 3.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Release notes
2
2
 
3
+ ## [3.0.1](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/3.0.1) - 25th March 2025
4
+
5
+ - Fix bug where color analysis didn't occur when using non-interactive mode
6
+
7
+ ## [3.0.0](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/3.0.0) - 25th March 2025
8
+
9
+ - `analyze` command updated to collect date about color usage
10
+ - For non-interactive command new flags introduced: `--color-usage` and `--component-usage`
11
+ - Improvement to usability of interactive `analyze` command
12
+ - Remove `--dry-run` from interactive `analyze`
13
+ - Additional prompt added if using an outdated version of the CLI tool
14
+ - Allow `track-package` command to use package where version is unspecified
15
+
3
16
  ## [2.4.3](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/2.4.3) - 2nd January 2025
4
17
 
5
18
  - `track-package --packages` option can be used to allow tracking specific packages within monorepos in non-interactive mode
package/README.md CHANGED
@@ -50,17 +50,27 @@ There are 4 levels of increasing severity:
50
50
 
51
51
  ---
52
52
 
53
+ ### Color usage analysis
54
+
55
+ [Help center article](https://zeroheight.com/help/article/color-usage/)
56
+
57
+ In the repository in which you wish to get usage metrics around how raw color values are being used, run the following command:
58
+
59
+ ```bash
60
+ zh-adoption analyze --color-usage
61
+ ```
62
+
53
63
  ### Component usage analysis
54
64
 
55
65
  [Help center article](https://zeroheight.com/help/article/component-usage/)
56
66
 
57
- In the React repository in which you wish to get usage metrics around how components from your design system packages are being used, run the following command:
67
+ In the **React** repository in which you wish to get usage metrics around how components from your design system packages are being used, run the following command:
58
68
 
59
69
  ```bash
60
- zh-adoption analyze
70
+ zh-adoption analyze --component-usage
61
71
  ```
62
72
 
63
- #### Options
73
+ ### Analyze command options
64
74
 
65
75
  `-e` / `--extensions`
66
76
 
@@ -72,7 +82,7 @@ zh-adoption analyze -e "**/*.{js,jsx,ts,tsx}"
72
82
 
73
83
  `-i` / `--ignore`
74
84
 
75
- Provide a glob pattern to ignore files, directories or file extensions when searching for components.
85
+ Provide a glob pattern to ignore files, directories or file extensions when analyzing.
76
86
 
77
87
  ```bash
78
88
  zh-adoption analyze -i "**/*.{test,spec}.*"
@@ -31,6 +31,17 @@ export type RawUsage = {
31
31
  package: string;
32
32
  props: ComponentProps;
33
33
  };
34
+ export type RawColorUsage = {
35
+ hex: string[];
36
+ rgb: string[];
37
+ hsla: string[];
38
+ oklab: string[];
39
+ hwb: string[];
40
+ lab: string[];
41
+ lch: string[];
42
+ colorSpace: string[];
43
+ totalCount: number;
44
+ };
34
45
  declare const RUNTIME_VALUE = "zh-runtime-value";
35
46
  export declare const ARRAY_VALUE = "zh-array-value";
36
47
  export declare const BINARY_VALUE = "zh-binary-value";
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  declare const program: Command;
4
+ export declare const CURRENT_VERSION = "3.0.1";
4
5
  export default program;
package/dist/cli.js CHANGED
@@ -3,18 +3,26 @@ import * as React from "react";
3
3
  import { Command, Option } from "commander";
4
4
  import { render } from "ink-render-string";
5
5
  import HelpInfo from "./components/help-info.js";
6
+ import LatestVersionInfo from "./components/latest-version-info.js";
6
7
  import { analyzeCommand } from "./commands/analyze.js";
7
8
  import { authCommand } from "./commands/auth.js";
8
9
  import { monitorRepoCommand } from "./commands/monitor-repo.js";
9
10
  import { trackPackageCommand } from "./commands/track-package.js";
10
11
  import logger, { setFileStream } from "./common/logging.js";
11
12
  const program = new Command();
12
- const { output, cleanup } = render(React.createElement(HelpInfo, null));
13
+ export const CURRENT_VERSION = "3.0.1";
14
+ async function getLatestPackageVersion() {
15
+ const response = await fetch("https://registry.npmjs.org/@zeroheight/adoption-cli/latest");
16
+ const json = await response.json();
17
+ return json.version;
18
+ }
19
+ const { output: helpOutput, cleanup: helpCleanup } = render(React.createElement(HelpInfo, null));
20
+ const { output: latestVersionOutput, cleanup: latestVersionCleanup } = render(React.createElement(LatestVersionInfo, { latestVersion: await getLatestPackageVersion() }));
13
21
  program
14
22
  .name("zh-adoption")
15
23
  .description("CLI for measuring design system usage usage in your products")
16
- .version("2.4.3")
17
- .addHelpText("before", output)
24
+ .version(CURRENT_VERSION)
25
+ .addHelpText("before", helpOutput)
18
26
  .option("--log-file <path>", "Path to write logs to, if not provided logs only error logs will be written to stderr")
19
27
  .addOption(new Option("--log-level <level>", "The lowest level of logs to display")
20
28
  .default("error")
@@ -23,7 +31,7 @@ program
23
31
  .addCommand(authCommand())
24
32
  .addCommand(monitorRepoCommand())
25
33
  .addCommand(trackPackageCommand())
26
- .hook("preAction", (thisCommand, actionCommand) => {
34
+ .hook("preAction", async (thisCommand, actionCommand) => {
27
35
  logger.level = thisCommand.opts()["logLevel"];
28
36
  if (thisCommand.opts()["logFile"]) {
29
37
  setFileStream(thisCommand.opts()["logFile"]);
@@ -35,8 +43,13 @@ program
35
43
  globalOpts: thisCommand.opts(),
36
44
  },
37
45
  }, "Running command");
46
+ const latestVersion = await getLatestPackageVersion();
47
+ if (latestVersion !== CURRENT_VERSION) {
48
+ console.log(latestVersionOutput);
49
+ }
38
50
  });
39
- cleanup();
51
+ helpCleanup();
52
+ latestVersionCleanup();
40
53
  // Only start parsing if run as CLI, don't start parsing during testing
41
54
  if (process.env["NODE_ENV"] !== "test") {
42
55
  program.parse();
@@ -1,14 +1,16 @@
1
1
  import { RenderOptions } from "ink";
2
2
  import { Command } from "commander";
3
- import { RawUsage } from "../ast/analyze.js";
3
+ import { RawColorUsage, RawUsage } from "../ast/analyze.js";
4
4
  interface AnalyzeOptions {
5
5
  extensions: string;
6
6
  ignore: string[];
7
- dryRun: boolean;
8
7
  repoName?: string;
9
8
  interactive: boolean;
9
+ componentUsage: boolean;
10
+ colorUsage: boolean;
10
11
  }
11
- export type RawUsageMap = Map<string, RawUsage[]>;
12
+ export type RawComponentUsageMap = Map<string, RawUsage[]>;
13
+ export type RawColorUsageMap = Map<string, RawColorUsage>;
12
14
  export declare function analyzeAction(options: AnalyzeOptions, renderOptions?: RenderOptions): Promise<void>;
13
15
  export declare function analyzeCommand(): Command;
14
16
  export {};
@@ -2,16 +2,16 @@ import * as React from "react";
2
2
  import { render } from "ink";
3
3
  import { Command, Option } from "commander";
4
4
  import yn from "yn";
5
- import { analyzeFiles, parseGlobList } from "./analyze.utils.js";
5
+ import { analyzeFiles, analyzeRawColorUsage, parseGlobList, } from "./analyze.utils.js";
6
6
  import Analyze from "../components/analyze/analyze.js";
7
7
  import NonInteractiveAnalyze from "../components/analyze/non-interactive-analyze.js";
8
8
  import logger, { setStdErrStream } from "../common/logging.js";
9
9
  export async function analyzeAction(options, renderOptions) {
10
10
  if (options.interactive) {
11
- render(React.createElement(Analyze, { dryRun: options.dryRun, onAnalyzeFiles: () => analyzeFiles(options.extensions, options.ignore), repoName: options.repoName }), renderOptions);
11
+ render(React.createElement(Analyze, { onAnalyzeFiles: () => analyzeFiles(options.extensions, options.ignore), onAnalyzeColorUsage: () => analyzeRawColorUsage(options.extensions, options.ignore), repoName: options.repoName }), renderOptions);
12
12
  }
13
13
  else {
14
- render(React.createElement(NonInteractiveAnalyze, { repoName: options.repoName, onAnalyzeFiles: () => analyzeFiles(options.extensions, options.ignore) }));
14
+ render(React.createElement(NonInteractiveAnalyze, { repoName: options.repoName, onAnalyzeFiles: () => analyzeFiles(options.extensions, options.ignore), onAnalyzeColorUsage: () => analyzeRawColorUsage(options.extensions, options.ignore), shouldAnalyzeComponentUsage: options.componentUsage, shouldAnalyzeTokenUsage: options.colorUsage }));
15
15
  }
16
16
  }
17
17
  export function analyzeCommand() {
@@ -26,11 +26,16 @@ export function analyzeCommand() {
26
26
  .addOption(new Option("-i, --ignore [patterns]", "files to ignore when searching for components, use multiple times to add more than one glob pattern")
27
27
  .default(["**/*.{test,spec}.*", "**/*.d.ts"], "*.test.*, *.spec.* and *.d.ts files")
28
28
  .argParser(parseGlobList))
29
- .addOption(new Option("-d, --dry-run", "don't push results to zeroheight").default(false))
30
29
  .addOption(new Option("-r, --repo-name <string>", "name of the repository"))
31
30
  .addOption(new Option("-in, --interactive [boolean]", "disable to skip input (useful when running in CI)")
32
31
  .default(true)
33
32
  .argParser((value) => yn(value)))
33
+ .addOption(new Option("-cu, --component-usage [boolean]", "gather data about component and prop usage")
34
+ .default(true)
35
+ .argParser((value) => yn(value)))
36
+ .addOption(new Option("-cou, --color-usage [boolean]", "gather data about color usage")
37
+ .default(true)
38
+ .argParser((value) => yn(value)))
34
39
  .action(async (options) => {
35
40
  if (!options.interactive) {
36
41
  setStdErrStream();
@@ -1,13 +1,6 @@
1
1
  import type { Option } from "commander";
2
- import { RawUsageMap } from "./analyze.js";
3
- import { ComponentProps } from "../ast/analyze.js";
4
- export interface ComponentUsageRecord {
5
- name: string;
6
- files: string[];
7
- count: number;
8
- package: string;
9
- props: ComponentProps;
10
- }
2
+ import { RawColorUsageMap, RawComponentUsageMap } from "./analyze.js";
3
+ import { RawColorUsage } from "../ast/analyze.js";
11
4
  /**
12
5
  * Get a list of files matching extensions, skips hidden files and node_modules
13
6
  * @param base starting directory
@@ -19,9 +12,15 @@ export interface ComponentUsageRecord {
19
12
  export declare function findFiles(base: string, extensions: string, ignorePattern: string[]): Promise<string[]>;
20
13
  export declare function analyzeFiles(extensions: string, ignorePattern: string[]): Promise<{
21
14
  errorFile: string | null;
22
- usage: RawUsageMap;
15
+ usage: RawComponentUsageMap;
16
+ }>;
17
+ export declare function calculateNumberOfComponents(usageResult: RawComponentUsageMap): number;
18
+ export declare function calculateNumberOfColors(usageResult: RawColorUsageMap): number;
19
+ export declare function countColorOccurrences(fileContent: string): RawColorUsage;
20
+ export declare function analyzeRawColorUsage(extensions: string, ignorePattern: string[]): Promise<{
21
+ errorFile: string | null;
22
+ usage: RawColorUsageMap;
23
23
  }>;
24
- export declare function calculateNumberOfComponents(usageResult: RawUsageMap): number;
25
24
  /**
26
25
  * Parse the ignore array and format correctly
27
26
  */
@@ -101,6 +101,93 @@ export function calculateNumberOfComponents(usageResult) {
101
101
  }
102
102
  return 0;
103
103
  }
104
+ export function calculateNumberOfColors(usageResult) {
105
+ if (!usageResult)
106
+ return 0;
107
+ const colors = Array.from(usageResult.entries()).flatMap((cols) => cols[1]);
108
+ const colorCount = colors.reduce((prev, next) => {
109
+ return prev + next.totalCount;
110
+ }, 0);
111
+ return colorCount;
112
+ }
113
+ export function countColorOccurrences(fileContent) {
114
+ // Matches #FFF, #FFFA, #FFFFFF or #FFFFFFA
115
+ const hexColorRegex = /#([a-fA-F0-9]{6}|[a-fA-F0-9]{8})\b|#([a-fA-F0-9]{3})\b/gi;
116
+ const hexMatches = Array.from(fileContent.match(hexColorRegex) ?? []);
117
+ // Matches rgb(255, 255, 255) or rgba(255, 255, 255) (with or without commas)
118
+ const rgbColorRegex = /rgba?\(\s*(\d{1,3})\s*[,\s]\s*(\d{1,3})\s*[,\s]\s*(\d{1,3})(?:\s*[,\s]\s*(\d*\.?\d+))?\s*\)/g;
119
+ const rgbMatches = Array.from(fileContent.match(rgbColorRegex) ?? []);
120
+ // Matches hsl and hsla values
121
+ const hslaColorRegex = /hsl(?:a)?\(\s*(?:(?:none|from\s+[a-zA-Z#0-9()]+)?\s*(?:[a-z\d+\-*/%()\s]+)\s*|(\d+(?:deg|grad|rad|turn)?)\s*(\d+%?)\s*(\d+%?)\s*(?:\/\s*(\d+%?|0?\.\d+))?|(\d+(?:deg|grad|rad|turn)?)\s*,\s*(\d+%)\s*,\s*(\d+%)\s*(?:,\s*(\d+(\.\d+)?%?))?)\)/g;
122
+ const hslaMatches = Array.from(fileContent.match(hslaColorRegex) ?? []);
123
+ // Matches oklab and oklch values (with or without commas and opacity)
124
+ const oklabColorRegex = /okl(?:ab|ch)\(\s*(-?\d*\.?\d+)\s*[,\s]\s*(-?\d*\.?\d+)\s*[,\s]\s*(-?\d*\.?\d+)(?:\s*[,\s]\s*(\d*\.?\d+))?\s*\)/g;
125
+ const oklabMatches = Array.from(fileContent.match(oklabColorRegex) ?? []);
126
+ // Matches hwb values (with or without commas and opacity)
127
+ const hwbColorRegex = /hwb\(\s*(\d*\.?\d+)(?:deg|turn|rad|grad)?\s*[,\s]\s*(\d*\.?\d+%)\s*[,\s]\s*(\d*\.?\d+%)(?:\s*[,\s]\s*(\d*\.?\d+))?\s*\)/g;
128
+ const hwbMatches = Array.from(fileContent.match(hwbColorRegex) ?? []);
129
+ // Matches lab values (with or without commas and opacity)
130
+ const labColorRegex = /(?<!ok)lab\(\s*(-?\d*\.?\d+%)\s*[,\s]\s*(-?\d*\.?\d+)\s*[,\s]\s*(-?\d*\.?\d+)(?:\s*[,\s]\s*(\d*\.?\d+))?\s*\)/g;
131
+ const labMatches = Array.from(fileContent.match(labColorRegex) ?? []);
132
+ // Matches lch values (with or without commas and opacity)
133
+ const lchColorRegex = /(?<!ok)lch\(\s*(-?\d*\.?\d+%)\s*[,\s]\s*(-?\d*\.?\d+)\s*[,\s]\s*(-?\d*\.?\d+)(?:\s*[,\s]\s*(\d*\.?\d+))?\s*\)/g;
134
+ const lchMatches = Array.from(fileContent.match(lchColorRegex) ?? []);
135
+ // Matches color space values
136
+ const colorSpaceColorRegex = /color\(\s*([\w-]+)\s+(?:[-+]?\d*\.?\d+(?:e[-+]?\d+)?%?\s*){3,4}\)/g;
137
+ const colorSpaceMatches = Array.from(fileContent.match(colorSpaceColorRegex) ?? []);
138
+ return {
139
+ hex: hexMatches,
140
+ rgb: rgbMatches,
141
+ hsla: hslaMatches,
142
+ oklab: oklabMatches,
143
+ hwb: hwbMatches,
144
+ lab: labMatches,
145
+ lch: lchMatches,
146
+ colorSpace: colorSpaceMatches,
147
+ totalCount: hexMatches.length +
148
+ rgbMatches.length +
149
+ hslaMatches.length +
150
+ hwbMatches.length +
151
+ labMatches.length +
152
+ lchMatches.length +
153
+ colorSpaceMatches.length +
154
+ oklabMatches.length,
155
+ };
156
+ }
157
+ export async function analyzeRawColorUsage(extensions, ignorePattern) {
158
+ const files = await findFiles(process.cwd(), extensions, ignorePattern);
159
+ if (files.length === 0) {
160
+ throw new Error("Can't find any relevant files");
161
+ }
162
+ const parseErrors = [];
163
+ const usageMap = new Map();
164
+ for (const file of files) {
165
+ try {
166
+ const fileContents = await readFile(file, "utf-8");
167
+ const colorUsage = countColorOccurrences(fileContents);
168
+ if (colorUsage.totalCount > 0) {
169
+ const relativePath = file.slice(process.cwd().length);
170
+ usageMap.set(relativePath, colorUsage);
171
+ }
172
+ }
173
+ catch (e) {
174
+ logger.error({ file, error: e }, "Error parsing file");
175
+ parseErrors.push(`Can't parse file ${file}`);
176
+ }
177
+ }
178
+ const errorFile = `/tmp/zh-adoption-analyze-errors-${Date.now()}`;
179
+ if (parseErrors.length > 0) {
180
+ const file = fs.createWriteStream(errorFile);
181
+ parseErrors.forEach((err) => {
182
+ file.write(err + "\n");
183
+ });
184
+ file.end();
185
+ }
186
+ return {
187
+ errorFile: parseErrors.length > 0 ? errorFile : null,
188
+ usage: usageMap,
189
+ };
190
+ }
104
191
  /**
105
192
  * Parse the ignore array and format correctly
106
193
  */
@@ -32,7 +32,8 @@ export async function getPackageInfo(allowedPackages) {
32
32
  return {
33
33
  name: parsedPackage.name,
34
34
  path: `.${file.split(base).pop()}`,
35
- version: parsedPackage.version,
35
+ // Default to 0.0.0 as we don't have a version so it might be a workspaces project/monorepo
36
+ version: parsedPackage.version ?? "0.0.0",
36
37
  exports: parsedPackage.exports,
37
38
  };
38
39
  }));
@@ -1,6 +1,20 @@
1
1
  import { ComponentProps } from "../ast/analyze.js";
2
- import { RawUsageMap } from "../commands/analyze.js";
2
+ import { RawColorUsageMap, RawComponentUsageMap } from "../commands/analyze.js";
3
3
  import { Credentials } from "./config.js";
4
+ export interface ComponentUsageRecord {
5
+ name: string;
6
+ files: string[];
7
+ count: number;
8
+ package: string;
9
+ props: ComponentProps;
10
+ }
11
+ export interface TokenLiteralUsageRecord {
12
+ value: string;
13
+ files: string[];
14
+ /** Only support color values right now */
15
+ type: "color";
16
+ count: number;
17
+ }
4
18
  export declare enum ResponseStatus {
5
19
  Success = "success",
6
20
  Error = "error",
@@ -73,7 +87,11 @@ interface ComponentUsageDetailsSuccess {
73
87
  repo_name: string;
74
88
  };
75
89
  }
76
- export declare function submitUsageData(usage: RawUsageMap, repoName: string, credentials: Credentials): Promise<APIResponse<ComponentUsageDetailsSuccess>>;
90
+ export declare function submitComponentUsageData(usage: RawComponentUsageMap, repoName: string, credentials: Credentials): Promise<APIResponse<ComponentUsageDetailsSuccess>>;
91
+ interface TokenLiteralUsageDetailsSuccess {
92
+ token_literal_usage: {};
93
+ }
94
+ export declare function submitTokenLiteralUsageData(usage: RawColorUsageMap, repoName: string, credentials: Credentials): Promise<APIResponse<TokenLiteralUsageDetailsSuccess>>;
77
95
  interface RepoNamesSuccess {
78
96
  repo_names: string[];
79
97
  }
@@ -31,12 +31,20 @@ export async function submitMonitoredRepoDetails(name, version, lockfilePath, pa
31
31
  version,
32
32
  }, credentials);
33
33
  }
34
- export async function submitUsageData(usage, repoName, credentials) {
34
+ export async function submitComponentUsageData(usage, repoName, credentials) {
35
35
  return post("/component_usages", {
36
36
  repo_name: repoName,
37
37
  usage: transformUsageByName(usage),
38
38
  }, credentials);
39
39
  }
40
+ export async function submitTokenLiteralUsageData(usage, repoName, credentials) {
41
+ return post("/token_literal_usages", {
42
+ token_literal_usage: {
43
+ repo_name: repoName,
44
+ usage: transformTokenLiteralUsageByName(usage),
45
+ },
46
+ }, credentials);
47
+ }
40
48
  export async function getExistingRepoNames(credentials) {
41
49
  return get("/component_usages/repo_names", credentials);
42
50
  }
@@ -141,3 +149,21 @@ function transformUsageByName(usage) {
141
149
  }
142
150
  return Array.from(transformedUsageMap.values());
143
151
  }
152
+ function transformTokenLiteralUsageByName(usage) {
153
+ const transformedUsageMap = new Map();
154
+ for (const [file, { hex, rgb, hsla }] of usage) {
155
+ // These values shouldn't collide as they're all different notations and types
156
+ const values = [...hex, ...rgb, ...hsla];
157
+ for (const colorValue of values) {
158
+ const currentValue = transformedUsageMap.get(colorValue);
159
+ const newFileList = [...(currentValue?.files ?? []), file];
160
+ transformedUsageMap.set(colorValue, {
161
+ value: colorValue,
162
+ type: "color",
163
+ files: newFileList,
164
+ count: (currentValue?.count ?? 0) + 1,
165
+ });
166
+ }
167
+ }
168
+ return Array.from(transformedUsageMap.values());
169
+ }
@@ -1,11 +1,14 @@
1
1
  import React from "react";
2
- import { RawUsageMap } from "../../commands/analyze.js";
2
+ import { RawColorUsageMap, RawComponentUsageMap } from "../../commands/analyze.js";
3
3
  export interface AnalyzeProps {
4
4
  onAnalyzeFiles: () => Promise<{
5
5
  errorFile: string | null;
6
- usage: RawUsageMap;
6
+ usage: RawComponentUsageMap;
7
+ }>;
8
+ onAnalyzeColorUsage: () => Promise<{
9
+ errorFile: string | null;
10
+ usage: RawColorUsageMap;
7
11
  }>;
8
- dryRun: boolean;
9
12
  repoName?: string;
10
13
  }
11
- export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }: AnalyzeProps): React.JSX.Element;
14
+ export default function Analyze({ onAnalyzeFiles, onAnalyzeColorUsage, repoName, }: AnalyzeProps): React.JSX.Element;
@@ -8,48 +8,116 @@ import NoCredentialsOnboarding from "../auth/no-credentials-onboarding.js";
8
8
  import RepoNamePrompt from "../repo-name-prompt.js";
9
9
  import UsageTable from "../usage-table.js";
10
10
  import { configPath, writeConfig, readConfig, } from "../../common/config.js";
11
- import { ResponseStatus, getAuthDetails, getZeroheightURL, submitUsageData, } from "../../common/api.js";
12
- import { calculateNumberOfComponents } from "../../commands/analyze.utils.js";
11
+ import { ResponseStatus, getAuthDetails, getZeroheightURL, submitComponentUsageData, submitTokenLiteralUsageData, } from "../../common/api.js";
12
+ import { calculateNumberOfColors, calculateNumberOfComponents, } from "../../commands/analyze.utils.js";
13
13
  import { ApiError } from "../../common/errors.js";
14
- export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
14
+ import ColorUsageTable from "../color-usage-table.js";
15
+ var AnalyzeStage;
16
+ (function (AnalyzeStage) {
17
+ AnalyzeStage[AnalyzeStage["initial"] = 0] = "initial";
18
+ AnalyzeStage[AnalyzeStage["finished"] = 1] = "finished";
19
+ AnalyzeStage[AnalyzeStage["fetchCredentials"] = 2] = "fetchCredentials";
20
+ AnalyzeStage[AnalyzeStage["fetchRepoNames"] = 3] = "fetchRepoNames";
21
+ AnalyzeStage[AnalyzeStage["cuScanningFiles"] = 4] = "cuScanningFiles";
22
+ AnalyzeStage[AnalyzeStage["cuScanComplete"] = 5] = "cuScanComplete";
23
+ AnalyzeStage[AnalyzeStage["cuShowMoreDetails"] = 6] = "cuShowMoreDetails";
24
+ AnalyzeStage[AnalyzeStage["cuSendDataCheck"] = 7] = "cuSendDataCheck";
25
+ AnalyzeStage[AnalyzeStage["cuAuthSetUp"] = 8] = "cuAuthSetUp";
26
+ AnalyzeStage[AnalyzeStage["cuRepoSelection"] = 9] = "cuRepoSelection";
27
+ AnalyzeStage[AnalyzeStage["cuSendingData"] = 10] = "cuSendingData";
28
+ AnalyzeStage[AnalyzeStage["cuSendSuccess"] = 11] = "cuSendSuccess";
29
+ AnalyzeStage[AnalyzeStage["tuConfirmRun"] = 12] = "tuConfirmRun";
30
+ AnalyzeStage[AnalyzeStage["tuScanningFiles"] = 13] = "tuScanningFiles";
31
+ AnalyzeStage[AnalyzeStage["tuScanComplete"] = 14] = "tuScanComplete";
32
+ AnalyzeStage[AnalyzeStage["tuShowMoreDetails"] = 15] = "tuShowMoreDetails";
33
+ AnalyzeStage[AnalyzeStage["tuSendDataCheck"] = 16] = "tuSendDataCheck";
34
+ AnalyzeStage[AnalyzeStage["tuAuthSetUp"] = 17] = "tuAuthSetUp";
35
+ AnalyzeStage[AnalyzeStage["tuRepoSelection"] = 18] = "tuRepoSelection";
36
+ AnalyzeStage[AnalyzeStage["tuSendingData"] = 19] = "tuSendingData";
37
+ AnalyzeStage[AnalyzeStage["tuSendSuccess"] = 20] = "tuSendSuccess";
38
+ })(AnalyzeStage || (AnalyzeStage = {}));
39
+ export default function Analyze({ onAnalyzeFiles, onAnalyzeColorUsage, repoName, }) {
15
40
  const { exit } = useApp();
16
- const [usageResult, setUsageResult] = React.useState(null);
17
- const [isSendingData, setIsSendingData] = React.useState(false);
41
+ const [currentStage, setCurrentStage] = React.useState(AnalyzeStage.initial);
42
+ // Result states
43
+ const [componentUsageResult, setComponentUsageResult] = React.useState(new Map());
44
+ const [colorUsageResult, setColorUsageResult] = React.useState(new Map());
18
45
  const [errorList, setErrorList] = React.useState([]);
19
46
  const [errorFileLocation, setErrorFileLocation] = React.useState(null);
47
+ // Set up states
20
48
  const [credentials, setCredentials] = React.useState(null);
21
- const [resourceURL, setResourceURL] = React.useState(null);
49
+ const [resourceURL, setResourceURL] = React.useState(getZeroheightURL());
22
50
  const [repo, setRepo] = React.useState(repoName);
23
- const [promptRepo, setPromptRepo] = React.useState(repoName === undefined);
24
- const [showContinue, setShowContinue] = React.useState(false);
51
+ // Text input states
52
+ const [continueToTokenUsage, setContinueToTokenUsage] = React.useState("");
53
+ const [runComponentUsage, setRunComponentUsage] = React.useState("");
54
+ const [runTokenUsage, setRunTokenUsage] = React.useState("");
25
55
  const [showMoreResults, setShowMoreResults] = React.useState("");
26
- React.useEffect(() => {
27
- (async () => {
28
- try {
29
- const { errorFile, usage } = await onAnalyzeFiles();
30
- setErrorFileLocation(errorFile);
31
- setUsageResult(usage);
32
- }
33
- catch (e) {
34
- setErrorList((s) => [...s, e]);
56
+ // Component usage
57
+ function handleStartComponentUsage(shouldScan) {
58
+ if (shouldScan) {
59
+ setCurrentStage(AnalyzeStage.cuScanningFiles);
60
+ analyzeComponentUsage();
61
+ }
62
+ else {
63
+ setCurrentStage(AnalyzeStage.tuConfirmRun);
64
+ }
65
+ }
66
+ async function analyzeComponentUsage() {
67
+ try {
68
+ const { errorFile, usage } = await onAnalyzeFiles();
69
+ setErrorFileLocation(errorFile);
70
+ setComponentUsageResult(usage);
71
+ setCurrentStage(AnalyzeStage.cuScanComplete);
72
+ }
73
+ catch (e) {
74
+ setErrorList((s) => [...s, e]);
75
+ }
76
+ }
77
+ function handleShowMore(showMore) {
78
+ if (showMore) {
79
+ setCurrentStage(AnalyzeStage.cuShowMoreDetails);
80
+ }
81
+ else {
82
+ setCurrentStage(AnalyzeStage.cuSendDataCheck);
83
+ }
84
+ }
85
+ async function handleSendCuData(shouldSend) {
86
+ if (shouldSend) {
87
+ const creds = await fetchCredentials();
88
+ if (!creds) {
89
+ setCurrentStage(AnalyzeStage.cuAuthSetUp);
90
+ return;
35
91
  }
36
- if (dryRun)
92
+ if (!repo) {
93
+ setCurrentStage(AnalyzeStage.cuRepoSelection);
37
94
  return;
38
- try {
39
- const config = await readConfig();
40
- if (config) {
41
- setCredentials({ token: config.token, client: config.client });
42
- }
43
95
  }
44
- catch (e) {
45
- setErrorList((s) => [...s, "Failed to load credentials"]);
96
+ setCurrentStage(AnalyzeStage.cuSendingData);
97
+ sendCuData(componentUsageResult, repo, creds);
98
+ }
99
+ else {
100
+ setCurrentStage(AnalyzeStage.tuConfirmRun);
101
+ exit();
102
+ }
103
+ }
104
+ async function fetchCredentials() {
105
+ try {
106
+ const config = await readConfig();
107
+ if (config) {
108
+ setCredentials({ token: config.token, client: config.client });
109
+ return { token: config.token, client: config.client };
46
110
  }
47
- })();
48
- }, []);
49
- async function sendData(result, repoName, credentials) {
50
- setIsSendingData(true);
111
+ return null;
112
+ }
113
+ catch (e) {
114
+ setErrorList((s) => [...s, "Failed to load credentials"]);
115
+ return null;
116
+ }
117
+ }
118
+ async function sendCuData(result, repoName, credentials) {
51
119
  try {
52
- await submitUsageData(result, repoName, credentials);
120
+ await submitComponentUsageData(result, repoName, credentials);
53
121
  const authDetailsResponse = await getAuthDetails(credentials);
54
122
  if (authDetailsResponse.status !== ResponseStatus.Success) {
55
123
  throw new Error("Failed to get credentials");
@@ -60,6 +128,7 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
60
128
  ? `/project/${projectId}/adoption/`
61
129
  : "/adoption/";
62
130
  setResourceURL(resourceURL);
131
+ setCurrentStage(AnalyzeStage.cuSendSuccess);
63
132
  }
64
133
  catch (e) {
65
134
  let errorMessage = "Failed to send data to zeroheight";
@@ -68,23 +137,6 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
68
137
  }
69
138
  setErrorList((s) => [...s, errorMessage]);
70
139
  }
71
- finally {
72
- setIsSendingData(false);
73
- }
74
- }
75
- async function handleContinue(shouldContinue) {
76
- if (shouldContinue) {
77
- if (!dryRun && credentials && usageResult && !repo) {
78
- setPromptRepo(true);
79
- setRepo("");
80
- }
81
- else if (!dryRun && credentials && usageResult && repo) {
82
- sendData(usageResult, repo, credentials);
83
- }
84
- }
85
- else {
86
- exit();
87
- }
88
140
  }
89
141
  function handleOnRepoNameSelected(repoName) {
90
142
  setRepo(repoName);
@@ -93,14 +145,13 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
93
145
  [process.cwd()]: repoName,
94
146
  },
95
147
  });
96
- setPromptRepo(false);
97
- }
98
- function handleShowMore(showMore) {
99
- if (showMore) {
100
- setShowMoreResults("true");
148
+ if (currentStage === AnalyzeStage.cuRepoSelection) {
149
+ handleSendCuData(true);
150
+ setCurrentStage(AnalyzeStage.cuSendingData);
101
151
  }
102
152
  else {
103
- setShowContinue(true);
153
+ handleSendTuData(true);
154
+ setCurrentStage(AnalyzeStage.tuSendingData);
104
155
  }
105
156
  }
106
157
  async function handleCheckCredentials(newClient, newToken) {
@@ -112,6 +163,86 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
112
163
  return exit();
113
164
  }
114
165
  }
166
+ // Token usage
167
+ function startTokenUsageFlow(shouldContinue) {
168
+ if (shouldContinue) {
169
+ setCurrentStage(AnalyzeStage.tuConfirmRun);
170
+ }
171
+ else {
172
+ setCurrentStage(AnalyzeStage.finished);
173
+ }
174
+ }
175
+ function handleStartTokenUsage(shouldScan) {
176
+ if (shouldScan) {
177
+ setCurrentStage(AnalyzeStage.tuScanningFiles);
178
+ analyzeTokenUsage();
179
+ }
180
+ else {
181
+ setCurrentStage(AnalyzeStage.finished);
182
+ }
183
+ }
184
+ async function analyzeTokenUsage() {
185
+ try {
186
+ const { errorFile, usage } = await onAnalyzeColorUsage();
187
+ setErrorFileLocation(errorFile);
188
+ setColorUsageResult(usage);
189
+ setCurrentStage(AnalyzeStage.tuScanComplete);
190
+ }
191
+ catch (e) {
192
+ setErrorList((s) => [...s, e]);
193
+ }
194
+ }
195
+ function handleShowMoreTU(showMore) {
196
+ if (showMore) {
197
+ setCurrentStage(AnalyzeStage.tuShowMoreDetails);
198
+ }
199
+ else {
200
+ setCurrentStage(AnalyzeStage.tuSendDataCheck);
201
+ }
202
+ }
203
+ async function handleSendTuData(shouldSend) {
204
+ if (shouldSend) {
205
+ const creds = await fetchCredentials();
206
+ if (!creds) {
207
+ setCurrentStage(AnalyzeStage.tuAuthSetUp);
208
+ return;
209
+ }
210
+ if (!repo) {
211
+ setCurrentStage(AnalyzeStage.tuRepoSelection);
212
+ return;
213
+ }
214
+ setCurrentStage(AnalyzeStage.tuSendingData);
215
+ sendTuData(colorUsageResult, repo, creds);
216
+ }
217
+ else {
218
+ setCurrentStage(AnalyzeStage.finished);
219
+ exit();
220
+ }
221
+ }
222
+ async function sendTuData(result, repoName, credentials) {
223
+ try {
224
+ await submitTokenLiteralUsageData(result, repoName, credentials);
225
+ const authDetailsResponse = await getAuthDetails(credentials);
226
+ if (authDetailsResponse.status !== ResponseStatus.Success) {
227
+ throw new Error("Failed to get credentials");
228
+ }
229
+ const projectId = authDetailsResponse.data.project?.id;
230
+ const resourceURL = getZeroheightURL();
231
+ resourceURL.pathname = projectId
232
+ ? `/project/${projectId}/adoption/`
233
+ : "/adoption/";
234
+ setResourceURL(resourceURL);
235
+ setCurrentStage(AnalyzeStage.tuSendSuccess);
236
+ }
237
+ catch (e) {
238
+ let errorMessage = "Failed to send data to zeroheight";
239
+ if (e instanceof ApiError) {
240
+ errorMessage = e.message;
241
+ }
242
+ setErrorList((s) => [...s, errorMessage]);
243
+ }
244
+ }
245
+ // UI flow
115
246
  if (errorList.length > 0) {
116
247
  return (React.createElement(Box, { flexDirection: "column" },
117
248
  React.createElement(Text, { color: "red" }, "Error:"),
@@ -119,7 +250,69 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
119
250
  "- ",
120
251
  e))))));
121
252
  }
122
- if (!promptRepo && !credentials && !dryRun) {
253
+ if (currentStage === AnalyzeStage.initial) {
254
+ return (React.createElement(Box, { flexDirection: "column" },
255
+ React.createElement(Text, null,
256
+ "\uD83D\uDC4B Welcome to the ",
257
+ React.createElement(Text, { color: "#f63e7c" }, "zeroheight"),
258
+ " analyze tool"),
259
+ React.createElement(Newline, null),
260
+ React.createElement(Box, { flexDirection: "column" },
261
+ React.createElement(Text, null,
262
+ "\u2753 Would you like to us to scan the files in this project to understand how components are being used (React only)?",
263
+ " ",
264
+ React.createElement(Text, { dimColor: true }, "(Y/n)")),
265
+ React.createElement(ConfirmInput, { isChecked: true, value: runComponentUsage, onChange: setRunComponentUsage, onSubmit: handleStartComponentUsage }))));
266
+ }
267
+ if (currentStage === AnalyzeStage.cuScanningFiles) {
268
+ return (React.createElement(Text, null,
269
+ React.createElement(Text, { color: "green" },
270
+ React.createElement(Spinner, { type: "dots" })),
271
+ " Scanning files for component usage..."));
272
+ }
273
+ if (currentStage === AnalyzeStage.cuScanComplete) {
274
+ if (componentUsageResult?.size > 0) {
275
+ return (React.createElement(Box, { flexDirection: "column" },
276
+ React.createElement(Text, { color: "green" },
277
+ calculateNumberOfComponents(componentUsageResult),
278
+ " components found"),
279
+ errorFileLocation && (React.createElement(React.Fragment, null,
280
+ React.createElement(Newline, null),
281
+ React.createElement(Box, null,
282
+ React.createElement(Text, { color: "red" }, "Error:"),
283
+ React.createElement(Text, null,
284
+ " ",
285
+ "Some files could not be parsed. Errors can be found at",
286
+ " ",
287
+ React.createElement(Link, { url: errorFileLocation }, errorFileLocation))))),
288
+ React.createElement(Box, null,
289
+ React.createElement(Text, null,
290
+ "Show more details? ",
291
+ React.createElement(Text, { dimColor: true }, "(y/N)")),
292
+ React.createElement(ConfirmInput, { isChecked: false, value: showMoreResults, onChange: setShowMoreResults, onSubmit: handleShowMore }))));
293
+ }
294
+ else {
295
+ return (React.createElement(Box, { flexDirection: "column" },
296
+ React.createElement(Box, null,
297
+ React.createElement(Text, null, "No files found, try a different directory or change the file extensions you want to include.")),
298
+ React.createElement(Box, null,
299
+ React.createElement(Text, null,
300
+ "Continue to token usage analysis? ",
301
+ React.createElement(Text, { dimColor: true }, "(Y/n)")),
302
+ React.createElement(ConfirmInput, { isChecked: true, value: continueToTokenUsage, onChange: setContinueToTokenUsage, onSubmit: startTokenUsageFlow }))));
303
+ }
304
+ }
305
+ if (currentStage === AnalyzeStage.cuShowMoreDetails) {
306
+ return (React.createElement(Box, { flexDirection: "column" },
307
+ React.createElement(UsageTable, { usage: componentUsageResult }),
308
+ React.createElement(Newline, null),
309
+ React.createElement(ContinuePrompt, { onContinue: handleSendCuData })));
310
+ }
311
+ if (currentStage === AnalyzeStage.cuSendDataCheck) {
312
+ return React.createElement(ContinuePrompt, { onContinue: handleSendCuData });
313
+ }
314
+ if (currentStage === AnalyzeStage.cuAuthSetUp ||
315
+ currentStage === AnalyzeStage.tuAuthSetUp) {
123
316
  return (React.createElement(NoCredentialsOnboarding, { configPath: configPath(), onCheckCredentials: handleCheckCredentials, onSaveCredentials: async (persist, newClient, newToken) => {
124
317
  const newCredentials = { token: newToken, client: newClient };
125
318
  setCredentials(newCredentials);
@@ -129,83 +322,108 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
129
322
  token: newToken,
130
323
  });
131
324
  }
325
+ if (currentStage === AnalyzeStage.cuAuthSetUp) {
326
+ handleSendCuData(true);
327
+ setCurrentStage(AnalyzeStage.cuSendingData);
328
+ }
329
+ else {
330
+ handleSendTuData(true);
331
+ setCurrentStage(AnalyzeStage.tuSendingData);
332
+ }
132
333
  } }));
133
334
  }
134
- // Usage is being calculated
135
- if (usageResult === null) {
136
- return (React.createElement(Text, null,
137
- React.createElement(Text, { color: "green" },
138
- React.createElement(Spinner, { type: "dots" })),
139
- " Scanning files..."));
140
- }
141
- // No usage data found
142
- if (usageResult?.size === 0) {
143
- return (React.createElement(Text, null, "No files found, try a different directory or change the file extensions you want to include"));
335
+ if (currentStage === AnalyzeStage.cuRepoSelection ||
336
+ currentStage === AnalyzeStage.tuRepoSelection) {
337
+ return (React.createElement(RepoNamePrompt, { credentials: credentials, onRepoNameSelected: handleOnRepoNameSelected }));
144
338
  }
145
- // Posting usage data to zeroheight
146
- if (isSendingData) {
339
+ if (currentStage === AnalyzeStage.cuSendingData ||
340
+ currentStage === AnalyzeStage.tuSendingData) {
147
341
  return (React.createElement(Text, null,
148
342
  React.createElement(Text, { color: "green" },
149
343
  React.createElement(Spinner, { type: "dots" })),
150
- " Sending usage data to zeroheight..."));
344
+ " ",
345
+ "Sending usage data to ",
346
+ React.createElement(Text, { color: "#f63e7c" }, "zeroheight"),
347
+ "..."));
151
348
  }
152
- // Completed, usage data is ready to view on zeroheight
153
- if (resourceURL) {
154
- return (React.createElement(React.Fragment, null,
155
- React.createElement(Newline, null),
349
+ if (currentStage === AnalyzeStage.cuSendSuccess) {
350
+ return (React.createElement(Box, { flexDirection: "column" },
156
351
  React.createElement(Text, null,
157
- React.createElement(Text, { color: "green" }, "Successful:"),
352
+ React.createElement(Text, { color: "green" }, "Successfully sent:"),
158
353
  " View your",
159
354
  " ",
160
- React.createElement(Link, { url: resourceURL.toString() }, "usage data on zeroheight"))));
355
+ React.createElement(Link, { url: resourceURL.toString() },
356
+ React.createElement(Text, { underline: true, color: "#f63e7c" }, "usage data on zeroheight"))),
357
+ React.createElement(Newline, null),
358
+ React.createElement(Box, null,
359
+ React.createElement(Text, null,
360
+ "\u2753 Would you like to us to scan the files in this project to understand how colors are being used? ",
361
+ React.createElement(Text, { dimColor: true }, "(Y/n)")),
362
+ React.createElement(ConfirmInput, { isChecked: true, value: runTokenUsage, onChange: setRunTokenUsage, onSubmit: handleStartTokenUsage }))));
161
363
  }
162
- if (usageResult &&
163
- !promptRepo &&
164
- !(showMoreResults === "true" || showContinue)) {
364
+ if (currentStage === AnalyzeStage.tuConfirmRun) {
165
365
  return (React.createElement(Box, { flexDirection: "column" },
366
+ React.createElement(Box, null,
367
+ React.createElement(Text, null,
368
+ "\u2753 Would you like to us to scan the files in this project to understand how colors are being used? ",
369
+ React.createElement(Text, { dimColor: true }, "(Y/n)")),
370
+ React.createElement(ConfirmInput, { isChecked: true, value: runTokenUsage, onChange: setRunTokenUsage, onSubmit: handleStartTokenUsage }))));
371
+ }
372
+ if (currentStage === AnalyzeStage.tuScanningFiles) {
373
+ return (React.createElement(Text, null,
166
374
  React.createElement(Text, { color: "green" },
167
- calculateNumberOfComponents(usageResult),
168
- " components found"),
169
- errorFileLocation && (React.createElement(React.Fragment, null,
170
- React.createElement(Newline, null),
375
+ React.createElement(Spinner, { type: "dots" })),
376
+ " Scanning files for color usage..."));
377
+ }
378
+ if (currentStage === AnalyzeStage.tuScanComplete) {
379
+ if (colorUsageResult?.size > 0) {
380
+ return (React.createElement(Box, { flexDirection: "column" },
381
+ React.createElement(Text, { color: "green" },
382
+ calculateNumberOfColors(colorUsageResult),
383
+ " instatances of raw color usage found"),
384
+ errorFileLocation && (React.createElement(React.Fragment, null,
385
+ React.createElement(Newline, null),
386
+ React.createElement(Box, null,
387
+ React.createElement(Text, { color: "red" }, "Error:"),
388
+ React.createElement(Text, null,
389
+ " ",
390
+ "Some files could not be parsed. Errors can be found at",
391
+ " ",
392
+ React.createElement(Link, { url: errorFileLocation }, errorFileLocation))))),
171
393
  React.createElement(Box, null,
172
- React.createElement(Text, { color: "red" }, "Error:"),
173
394
  React.createElement(Text, null,
174
- " ",
175
- "Some files could not be parsed. Errors can be found at",
176
- " ",
177
- React.createElement(Link, { url: errorFileLocation }, errorFileLocation))))),
178
- React.createElement(Box, null,
179
- React.createElement(Text, null,
180
- "Show more details? ",
181
- React.createElement(Text, { dimColor: true }, "(y/N)")),
182
- React.createElement(ConfirmInput, { isChecked: false, value: showMoreResults, onChange: setShowMoreResults, onSubmit: handleShowMore }))));
395
+ "Show more details? ",
396
+ React.createElement(Text, { dimColor: true }, "(y/N)")),
397
+ React.createElement(ConfirmInput, { isChecked: false, value: showMoreResults, onChange: setShowMoreResults, onSubmit: handleShowMoreTU }))));
398
+ }
399
+ else {
400
+ return (React.createElement(Box, { flexDirection: "column" },
401
+ React.createElement(Text, null, "No files found, try a different directory or change the file extensions you want to include.")));
402
+ }
183
403
  }
184
- if (usageResult && !promptRepo && showMoreResults === "true") {
404
+ if (currentStage === AnalyzeStage.tuShowMoreDetails) {
185
405
  return (React.createElement(Box, { flexDirection: "column" },
186
- React.createElement(UsageTable, { usage: usageResult }),
187
- dryRun && (React.createElement(React.Fragment, null,
188
- React.createElement(Newline, null),
189
- React.createElement(Text, null,
190
- "Nothing sent to zeroheight, if you want to track component usage data then remove the ",
191
- React.createElement(Text, { italic: true }, "--dry-run"),
192
- " option."),
193
- React.createElement(Newline, null))),
194
- !dryRun && React.createElement(ContinuePrompt, { onContinue: handleContinue })));
406
+ React.createElement(ColorUsageTable, { usage: colorUsageResult }),
407
+ React.createElement(Newline, null),
408
+ React.createElement(ContinuePrompt, { onContinue: handleSendTuData })));
195
409
  }
196
- if (usageResult && !promptRepo && showContinue) {
410
+ if (currentStage === AnalyzeStage.tuSendDataCheck) {
411
+ return React.createElement(ContinuePrompt, { onContinue: handleSendTuData });
412
+ }
413
+ if (currentStage === AnalyzeStage.tuSendSuccess) {
197
414
  return (React.createElement(Box, { flexDirection: "column" },
198
- dryRun && (React.createElement(React.Fragment, null,
199
- React.createElement(Newline, null),
200
- React.createElement(Text, null,
201
- "Nothing sent to zeroheight, if you want to track component usage data then remove the ",
202
- React.createElement(Text, { italic: true }, "--dry-run"),
203
- " option."),
204
- React.createElement(Newline, null))),
205
- !dryRun && React.createElement(ContinuePrompt, { onContinue: handleContinue })));
415
+ React.createElement(Text, null,
416
+ React.createElement(Text, { color: "green" }, "Successfully sent:"),
417
+ " View your",
418
+ " ",
419
+ React.createElement(Link, { url: resourceURL.toString() },
420
+ React.createElement(Text, { underline: true, color: "#f63e7c" }, "usage data on zeroheight"))),
421
+ React.createElement(Newline, null),
422
+ React.createElement(Text, null, "\uD83D\uDC4B See you next time!")));
206
423
  }
207
- if (promptRepo) {
208
- return (React.createElement(RepoNamePrompt, { credentials: credentials, onRepoNameSelected: handleOnRepoNameSelected }));
424
+ if (currentStage === AnalyzeStage.finished) {
425
+ return (React.createElement(Box, { flexDirection: "column" },
426
+ React.createElement(Text, null, "\uD83D\uDC4B See you next time!")));
209
427
  }
210
- return React.createElement(Text, null, "Done");
428
+ return React.createElement(Text, null, "Analzye complete");
211
429
  }
@@ -1,10 +1,16 @@
1
1
  import React from "react";
2
- import { RawUsageMap } from "../../commands/analyze.js";
2
+ import { RawColorUsageMap, RawComponentUsageMap } from "../../commands/analyze.js";
3
3
  export interface NonInteractiveAnalyzeProps {
4
4
  onAnalyzeFiles: () => Promise<{
5
5
  errorFile: string | null;
6
- usage: RawUsageMap;
6
+ usage: RawComponentUsageMap;
7
+ }>;
8
+ onAnalyzeColorUsage: () => Promise<{
9
+ errorFile: string | null;
10
+ usage: RawColorUsageMap;
7
11
  }>;
8
12
  repoName?: string;
13
+ shouldAnalyzeComponentUsage: boolean;
14
+ shouldAnalyzeTokenUsage: boolean;
9
15
  }
10
- export default function NonInteractiveAnalyze({ repoName, onAnalyzeFiles, }: NonInteractiveAnalyzeProps): React.JSX.Element;
16
+ export default function NonInteractiveAnalyze({ repoName, onAnalyzeFiles, onAnalyzeColorUsage, shouldAnalyzeComponentUsage, shouldAnalyzeTokenUsage, }: NonInteractiveAnalyzeProps): React.JSX.Element;
@@ -2,18 +2,19 @@ import React from "react";
2
2
  import { Box, Newline, Text } from "ink";
3
3
  import Spinner from "ink-spinner";
4
4
  import Link from "ink-link";
5
- import { calculateNumberOfComponents } from "../../commands/analyze.utils.js";
5
+ import { calculateNumberOfColors, calculateNumberOfComponents, } from "../../commands/analyze.utils.js";
6
6
  import { readConfig } from "../../common/config.js";
7
- import { submitUsageData } from "../../common/api.js";
7
+ import { submitComponentUsageData, submitTokenLiteralUsageData, } from "../../common/api.js";
8
8
  import { ApiError } from "../../common/errors.js";
9
- export default function NonInteractiveAnalyze({ repoName, onAnalyzeFiles, }) {
9
+ export default function NonInteractiveAnalyze({ repoName, onAnalyzeFiles, onAnalyzeColorUsage, shouldAnalyzeComponentUsage, shouldAnalyzeTokenUsage, }) {
10
10
  const [isSendingData, setIsSendingData] = React.useState(false);
11
11
  const [isAnalyzingFiles, setIsAnalyzingFiles] = React.useState(false);
12
12
  const [isFetchingCredentials, setIsFetchingCredentials] = React.useState(false);
13
13
  const [errorFileLocation, setErrorFileLocation] = React.useState(null);
14
14
  const [errorList, setErrorList] = React.useState([]);
15
15
  const [credentials, setCredentials] = React.useState(null);
16
- const [usage, setUsage] = React.useState(null);
16
+ const [componentUsage, setComponentUsage] = React.useState(null);
17
+ const [colorUsage, setColorUsage] = React.useState(null);
17
18
  async function loadCredentials() {
18
19
  setIsFetchingCredentials(true);
19
20
  try {
@@ -31,13 +32,13 @@ export default function NonInteractiveAnalyze({ repoName, onAnalyzeFiles, }) {
31
32
  }
32
33
  return null;
33
34
  }
34
- async function sendData(result, repoName, credentials) {
35
+ async function sendComponentUsageData(result, repoName, credentials) {
35
36
  setIsSendingData(true);
36
37
  try {
37
- await submitUsageData(result, repoName, credentials);
38
+ await submitComponentUsageData(result, repoName, credentials);
38
39
  }
39
40
  catch (e) {
40
- let errorMessage = "Failed to send data to zeroheight";
41
+ let errorMessage = "Failed to send component usage data to zeroheight";
41
42
  if (e instanceof ApiError) {
42
43
  errorMessage = e.message;
43
44
  }
@@ -47,10 +48,36 @@ export default function NonInteractiveAnalyze({ repoName, onAnalyzeFiles, }) {
47
48
  setIsSendingData(false);
48
49
  }
49
50
  }
50
- async function anaylzeUsage() {
51
+ async function sendTokenUsageData(result, repoName, credentials) {
52
+ setIsSendingData(true);
53
+ try {
54
+ await submitTokenLiteralUsageData(result, repoName, credentials);
55
+ }
56
+ catch (e) {
57
+ let errorMessage = "Failed to send color usage data to zeroheight";
58
+ if (e instanceof ApiError) {
59
+ errorMessage = e.message;
60
+ }
61
+ setErrorList((s) => [...s, errorMessage]);
62
+ }
63
+ finally {
64
+ setIsSendingData(false);
65
+ }
66
+ }
67
+ async function analyzeComponentUsage() {
51
68
  setIsAnalyzingFiles(true);
52
69
  const { errorFile, usage } = await onAnalyzeFiles();
53
- setUsage(usage);
70
+ setComponentUsage(usage);
71
+ if (errorFile) {
72
+ setErrorFileLocation(errorFile);
73
+ }
74
+ setIsAnalyzingFiles(false);
75
+ return usage;
76
+ }
77
+ async function analyzeTokenUsage() {
78
+ setIsAnalyzingFiles(true);
79
+ const { errorFile, usage } = await onAnalyzeColorUsage();
80
+ setColorUsage(usage);
54
81
  if (errorFile) {
55
82
  setErrorFileLocation(errorFile);
56
83
  }
@@ -62,18 +89,25 @@ export default function NonInteractiveAnalyze({ repoName, onAnalyzeFiles, }) {
62
89
  const credentials = await loadCredentials();
63
90
  if (!credentials)
64
91
  return;
65
- const usage = await anaylzeUsage();
66
92
  if (!repoName)
67
93
  return;
68
- await sendData(usage, repoName, credentials);
94
+ if (shouldAnalyzeComponentUsage) {
95
+ const componentUsageMap = await analyzeComponentUsage();
96
+ await sendComponentUsageData(componentUsageMap, repoName, credentials);
97
+ }
98
+ if (shouldAnalyzeTokenUsage) {
99
+ const colorUsageMap = await analyzeTokenUsage();
100
+ await sendTokenUsageData(colorUsageMap, repoName, credentials);
101
+ }
69
102
  })();
70
103
  }, []);
71
104
  if (isAnalyzingFiles || isFetchingCredentials || isSendingData) {
72
105
  return (React.createElement(Text, null,
73
106
  React.createElement(Spinner, { type: "dots" }),
74
- isAnalyzingFiles && React.createElement(Text, null, "Analyzing components"),
107
+ " ",
108
+ isAnalyzingFiles && React.createElement(Text, null, "Analyzing files"),
75
109
  isFetchingCredentials && React.createElement(Text, null, "Loading credentials"),
76
- isSendingData && React.createElement(Text, null, "Sending data to zerohegiht")));
110
+ isSendingData && React.createElement(Text, null, "Sending data to zeroheight")));
77
111
  }
78
112
  if (errorList.length > 0) {
79
113
  return (React.createElement(Box, { flexDirection: "column" },
@@ -115,12 +149,16 @@ export default function NonInteractiveAnalyze({ repoName, onAnalyzeFiles, }) {
115
149
  " ",
116
150
  React.createElement(Text, { italic: true }, "--interactive false")));
117
151
  }
118
- if (!usage) {
152
+ if (!componentUsage && !colorUsage) {
119
153
  return React.createElement(Text, null, "No usage could be found.");
120
154
  }
121
- return (React.createElement(React.Fragment, null, usage && (React.createElement(Text, { color: "green" },
122
- calculateNumberOfComponents(usage),
123
- " components found and sent to",
124
- " ",
125
- React.createElement(Text, { color: "#f63e7c" }, "zeroheight")))));
155
+ return (React.createElement(React.Fragment, null,
156
+ componentUsage && (React.createElement(Text, { color: "green" },
157
+ calculateNumberOfComponents(componentUsage),
158
+ " components found and sent to ",
159
+ React.createElement(Text, { color: "#f63e7c" }, "zeroheight"))),
160
+ colorUsage && (React.createElement(Text, { color: "green" },
161
+ calculateNumberOfColors(colorUsage),
162
+ " usages of raw colors found and sent to ",
163
+ React.createElement(Text, { color: "#f63e7c" }, "zeroheight")))));
126
164
  }
@@ -22,7 +22,9 @@ export default function NoCredentialsOnboarding({ onCheckCredentials, onSaveCred
22
22
  React.createElement(Text, null,
23
23
  "If you need a new auth token, generate them from the",
24
24
  " ",
25
- React.createElement(Link, { url: "https://zeroheight.com/settings/team/developers" }, "Developer Settings")),
25
+ React.createElement(Link, { url: "https://zeroheight.com/settings/team/developers" },
26
+ React.createElement(Text, { underline: true, color: "#f63e7c" }, "Developer Settings"))),
27
+ React.createElement(Newline, null),
26
28
  field === "client" && (React.createElement(Box, null,
27
29
  React.createElement(Text, null, "Client ID: "),
28
30
  React.createElement(TextInput, { onChange: setClient, value: client, onSubmit: (v) => {
@@ -0,0 +1,7 @@
1
+ import * as React from "react";
2
+ import { RawColorUsageMap } from "../commands/analyze.js";
3
+ interface ColorUsageTableProps {
4
+ usage: RawColorUsageMap;
5
+ }
6
+ export default function ColorUsageTable({ usage }: ColorUsageTableProps): React.JSX.Element;
7
+ export {};
@@ -0,0 +1,38 @@
1
+ import * as React from "react";
2
+ import { Box, Text } from "ink";
3
+ const colorTypeNameMap = {
4
+ hex: "HEX",
5
+ rgb: "RGB",
6
+ hsla: "HSL",
7
+ oklab: "Oklab",
8
+ hwb: "HWB",
9
+ lab: "LAB",
10
+ lch: "LCH",
11
+ colorSpace: "Color space",
12
+ totalCount: "Total usage",
13
+ };
14
+ export default function ColorUsageTable({ usage }) {
15
+ const rows = Array.from(usage.entries());
16
+ return (React.createElement(Box, { flexDirection: "column" }, rows.map(([filepath, rawColorUsage]) => {
17
+ const rowValues = Object.entries(rawColorUsage);
18
+ return (React.createElement(Box, { key: filepath, flexDirection: "column" },
19
+ React.createElement(Text, { color: "green" }, filepath),
20
+ React.createElement(Box, { flexDirection: "column", marginLeft: 2 },
21
+ React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, rowValues.map(([type, values]) => {
22
+ if (type === "totalCount")
23
+ return null;
24
+ const count = values.length;
25
+ if (count === 0)
26
+ return null;
27
+ const name = colorTypeNameMap[type];
28
+ return (React.createElement(Box, { gap: 2, key: `${type}-${count}` },
29
+ React.createElement(Text, { bold: true },
30
+ name,
31
+ ": "),
32
+ React.createElement(Text, null, count)));
33
+ })),
34
+ React.createElement(Text, { bold: true },
35
+ "Total usage: ",
36
+ rawColorUsage.totalCount))));
37
+ })));
38
+ }
@@ -0,0 +1,9 @@
1
+ import * as React from "react";
2
+ interface LatestVersionInfoProps {
3
+ latestVersion: string;
4
+ }
5
+ /**
6
+ * Rich help banner with info about the latest version
7
+ */
8
+ export default function LatestVersionInfo({ latestVersion, }: LatestVersionInfoProps): React.JSX.Element;
9
+ export {};
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+ import { Box, Text } from "ink";
3
+ import Link from "ink-link";
4
+ import { CURRENT_VERSION } from "../cli.js";
5
+ /**
6
+ * Rich help banner with info about the latest version
7
+ */
8
+ export default function LatestVersionInfo({ latestVersion, }) {
9
+ const isMajorVersionOut = Number(CURRENT_VERSION.split(".")[0]) < Number(latestVersion.split(".")[0]);
10
+ return (React.createElement(React.Fragment, null,
11
+ React.createElement(Box, { borderStyle: "double", paddingLeft: 1, paddingRight: 1, marginBottom: 1, flexDirection: "column" },
12
+ React.createElement(Box, { justifyContent: "center", marginBottom: 1 },
13
+ React.createElement(Text, null, "\uD83D\uDC4B It seems you're not using the latest version of this package")),
14
+ React.createElement(Box, { flexDirection: "column", alignItems: "center", marginBottom: 1 },
15
+ React.createElement(Box, null,
16
+ React.createElement(Text, { color: isMajorVersionOut ? "redBright" : "blueBright", bold: true }, CURRENT_VERSION),
17
+ React.createElement(Text, { bold: true }, ` -> `),
18
+ React.createElement(Text, { color: "green", bold: true }, latestVersion)),
19
+ React.createElement(Box, null,
20
+ React.createElement(Text, null, "Update using: "),
21
+ React.createElement(Text, { bold: true }, "npm i @zeroheight/adoption-cli"))),
22
+ React.createElement(Box, { flexDirection: "column", alignItems: "center" },
23
+ React.createElement(Text, null,
24
+ "For more information on the latest version, check",
25
+ " ",
26
+ React.createElement(Link, { url: "https://www.npmjs.com/package/@zeroheight/adoption-cli" }, "here"))))));
27
+ }
@@ -5,7 +5,10 @@ export default function ContinuePrompt({ onContinue }) {
5
5
  const [shouldContinueText, setShouldContinueText] = React.useState("");
6
6
  return (React.createElement(Box, null,
7
7
  React.createElement(Text, null,
8
- "Continue? ",
8
+ "Send this data to ",
9
+ React.createElement(Text, { color: "#f63e7c" }, "zeroheight"),
10
+ "?",
11
+ " ",
9
12
  React.createElement(Text, { dimColor: true }, "(Y/n):")),
10
13
  React.createElement(ConfirmInput, { isChecked: true, value: shouldContinueText, onChange: setShouldContinueText, onSubmit: (answer) => {
11
14
  onContinue(answer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroheight/adoption-cli",
3
- "version": "2.4.3",
3
+ "version": "3.0.1",
4
4
  "license": "ISC",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {