@zeroheight/adoption-cli 2.0.1 → 2.1.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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Release notes
2
2
 
3
+ ## [2.1.0](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/2.1.0) - 8th November 2024
4
+
5
+ - Start collecting component property usage data as part of component analyzing
6
+
7
+ ## [2.0.2](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/2.0.2) - 29th October 2024
8
+
9
+ - Update readme
10
+
3
11
  ## [2.0.1](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/2.0.1) - 21st October 2024
4
12
 
5
13
  - Update help text and link
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  CLI for measuring component usage to view in your [zeroheight](https://zeroheight.com/) account.
4
4
 
5
- See release notes [here](https://www.npmjs.com/package/@zeroheight/adoption-cli?activeTab=code)
5
+ **For full walk through on how to use this CLI tool, head over to our [help centre article](https://zeroheight.com/help/article/adoption-private-beta-overview/).**
6
+
7
+ [Release notes](https://www.npmjs.com/package/@zeroheight/adoption-cli?activeTab=code)
6
8
 
7
9
  ## Install
8
10
 
@@ -37,7 +39,9 @@ export ZEROHEIGHT_ACCESS_TOKEN="your-access-token"
37
39
 
38
40
  ### Component usage analysis
39
41
 
40
- In the repository in which you wish to analyze the component usage, run the following command:
42
+ [Help center article](https://zeroheight.com/help/article/component-usage/)
43
+
44
+ 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:
41
45
 
42
46
  ```bash
43
47
  zh-adoption analyze
@@ -81,7 +85,11 @@ zh-adoption analyze --interactive false -r "My Repo"
81
85
 
82
86
  ### Monitor repo
83
87
 
84
- In the repository in which you wish to monitor the package versions being used, run the following command:
88
+ [Help center article](https://zeroheight.com/help/article/package-version-monitoring/)
89
+
90
+ This will analyze your code repository to help you understand what packages are being used and at what version.
91
+
92
+ In the repository in which you wish to monitor, run the following command:
85
93
 
86
94
  ```bash
87
95
  zh-adoption monitor-repo
@@ -101,7 +109,11 @@ zh-adoption monitor-repo -d ./webApp
101
109
 
102
110
  ### Track package
103
111
 
104
- In the repository containing the packages you wish to monitor the version of, run the following command:
112
+ [Help center article](https://zeroheight.com/help/article/package-version-monitoring/)
113
+
114
+ This will give zeroheight the name and current version of your design system package so you can compare it to the version being used by your other code repositories.
115
+
116
+ In the repository containing your design system package, run the following command:
105
117
 
106
118
  ```bash
107
119
  zh-adoption track-package
@@ -1,18 +1,39 @@
1
1
  import type { Node, Program, Identifier } from "acorn";
2
+ type AttrValueType = "JSXExpressionContainer" | "NumericLiteral" | "StringLiteral" | "BooleanLiteral" | "Identifier" | "TemplateLiteral" | "ArrowFunctionExpression" | "StaticMemberExpression" | "CallExpression" | "TSAsExpression" | "ObjectExpression" | "ArrayExpression" | "BinaryExpression" | undefined;
2
3
  export interface PlainIdentifier extends Node {
3
4
  type: "Identifier";
4
5
  local: Identifier;
5
6
  }
7
+ export interface Props {
8
+ name: string;
9
+ type?: AttrValueType[];
10
+ values: [string | number | typeof RUNTIME_VALUE | null | undefined];
11
+ }
6
12
  export type VisitorState = {
7
13
  components: Map<string, number>;
14
+ props: Map<string, Props[]>;
8
15
  imports: Map<string, {
9
16
  package: string;
10
17
  name: string;
11
18
  }>;
12
19
  };
20
+ type PropValues = string | number | null | undefined;
21
+ export type ComponentProps = {
22
+ [propName: string]: {
23
+ type?: AttrValueType[];
24
+ count?: number;
25
+ values: PropValues[];
26
+ };
27
+ };
13
28
  export type RawUsage = {
14
29
  name: string;
15
30
  count: number;
16
31
  package: string;
32
+ props: ComponentProps;
17
33
  };
34
+ declare const RUNTIME_VALUE = "zh-runtime-value";
35
+ export declare const ARRAY_VALUE = "zh-array-value";
36
+ export declare const BINARY_VALUE = "zh-binary-value";
37
+ export declare const OBJECT_VALUE = "zh-object-value";
18
38
  export declare function analyze(ast: Program): RawUsage[];
39
+ export {};
@@ -1,4 +1,16 @@
1
1
  import * as walk from "acorn-walk";
2
+ const DYNAMIC_EXPRESSIONS = [
3
+ "Identifier",
4
+ "TemplateLiteral",
5
+ "ArrowFunctionExpression",
6
+ "StaticMemberExpression",
7
+ "CallExpression",
8
+ "TSAsExpression",
9
+ ];
10
+ const RUNTIME_VALUE = "zh-runtime-value";
11
+ export const ARRAY_VALUE = "zh-array-value";
12
+ export const BINARY_VALUE = "zh-binary-value";
13
+ export const OBJECT_VALUE = "zh-object-value";
2
14
  function transformVisitorStateToRawUsage(visitorState) {
3
15
  const transformedUsageMap = new Map();
4
16
  for (const [name, count] of visitorState.components) {
@@ -10,10 +22,30 @@ function transformVisitorStateToRawUsage(visitorState) {
10
22
  name: actualName,
11
23
  count: 0,
12
24
  package: importInfo?.package ?? "",
25
+ props: {},
13
26
  };
14
27
  transformedUsageMap.set(key, {
15
28
  ...currentValue,
16
29
  count: currentValue.count + count,
30
+ props: {},
31
+ });
32
+ }
33
+ for (const [name, props] of visitorState.props) {
34
+ const importInfo = visitorState.imports.get(name);
35
+ const actualName = importInfo?.name ?? name;
36
+ const packageName = importInfo?.package ?? "";
37
+ const key = `package:${packageName} name:${actualName}`;
38
+ const currentValue = transformedUsageMap.get(key);
39
+ const componentProps = currentValue.props;
40
+ props.forEach((prop) => {
41
+ componentProps[prop.name] = {
42
+ type: Array.from(new Set([...(componentProps[prop.name]?.type ?? []), ...(prop.type ?? [])])),
43
+ values: [...(componentProps[prop.name]?.values ?? []), ...prop.values],
44
+ };
45
+ });
46
+ transformedUsageMap.set(key, {
47
+ ...currentValue,
48
+ props: componentProps,
17
49
  });
18
50
  }
19
51
  return Array.from(transformedUsageMap.values());
@@ -21,6 +53,7 @@ function transformVisitorStateToRawUsage(visitorState) {
21
53
  export function analyze(ast) {
22
54
  const visitorState = {
23
55
  components: new Map(),
56
+ props: new Map(),
24
57
  imports: new Map(),
25
58
  };
26
59
  const visitorFunctions = {
@@ -84,6 +117,63 @@ export function analyze(ast) {
84
117
  const elName = el.name.name;
85
118
  // Ignore html tags e.g. div, h1, span
86
119
  if (elName?.[0] !== elName?.[0]?.toLocaleLowerCase()) {
120
+ if (el.attributes.length > 0) {
121
+ const attrProps = [];
122
+ el.attributes.forEach((attr) => {
123
+ // We won't have a name if it is a JSXSpreadAttribute
124
+ if (attr.name) {
125
+ if (attr.value?.type === "JSXExpressionContainer") {
126
+ const expressionType = attr.value.expression.type;
127
+ // These are props determined at runtime so we cannot parse them
128
+ if (expressionType &&
129
+ DYNAMIC_EXPRESSIONS.includes(expressionType)) {
130
+ attrProps.push({
131
+ name: attr.name.name,
132
+ type: [expressionType],
133
+ values: [RUNTIME_VALUE],
134
+ });
135
+ }
136
+ else if (expressionType && expressionType === 'ObjectExpression') {
137
+ attrProps.push({
138
+ name: attr.name.name,
139
+ type: [expressionType],
140
+ values: [OBJECT_VALUE],
141
+ });
142
+ }
143
+ else if (expressionType && expressionType === 'ArrayExpression') {
144
+ attrProps.push({
145
+ name: attr.name.name,
146
+ type: [expressionType],
147
+ values: [ARRAY_VALUE],
148
+ });
149
+ }
150
+ else if (expressionType && expressionType === 'BinaryExpression') {
151
+ attrProps.push({
152
+ name: attr.name.name,
153
+ type: [expressionType],
154
+ values: [BINARY_VALUE],
155
+ });
156
+ }
157
+ else {
158
+ attrProps.push({
159
+ name: attr.name.name,
160
+ type: [expressionType],
161
+ values: [attr.value.expression.value || attr.value.expression.name],
162
+ });
163
+ }
164
+ }
165
+ else {
166
+ attrProps.push({
167
+ name: attr.name.name,
168
+ type: [attr.value?.type],
169
+ values: [attr.value?.value],
170
+ });
171
+ }
172
+ }
173
+ });
174
+ const existingProps = state.props.get(elName) ?? [];
175
+ state.props.set(elName, [...attrProps, ...existingProps]);
176
+ }
87
177
  state.components.set(elName, (state.components.get(elName) ?? 0) + 1);
88
178
  }
89
179
  node.children.forEach((child) => {
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ const { output, cleanup } = render(React.createElement(HelpInfo, null));
12
12
  program
13
13
  .name("zh-adoption")
14
14
  .description("CLI for measuring design system usage usage in your products")
15
- .version("2.0.1")
15
+ .version("2.1.0")
16
16
  .addHelpText("before", output)
17
17
  .addCommand(analyzeCommand())
18
18
  .addCommand(authCommand())
@@ -1,9 +1,11 @@
1
1
  import { RawUsageMap } from "./analyze.js";
2
+ import { ComponentProps } from "../ast/analyze.js";
2
3
  export interface ComponentUsageRecord {
3
4
  name: string;
4
5
  files: string[];
5
6
  count: number;
6
7
  package: string;
8
+ props: ComponentProps;
7
9
  }
8
10
  /**
9
11
  * Get a list of files matching extensions, skips hidden files and node_modules
@@ -1,3 +1,4 @@
1
+ import { ComponentProps } from "../ast/analyze.js";
1
2
  import { RawUsageMap } from "../commands/analyze.js";
2
3
  import { Credentials } from "./config.js";
3
4
  export declare enum ResponseStatus {
@@ -73,10 +74,10 @@ interface ComponentUsageDetailsSuccess {
73
74
  };
74
75
  }
75
76
  export declare function submitUsageData(usage: RawUsageMap, repoName: string, credentials: Credentials): Promise<APIResponse<ComponentUsageDetailsSuccess>>;
76
- interface RepoNamesSuccss {
77
+ interface RepoNamesSuccess {
77
78
  repo_names: string[];
78
79
  }
79
- export declare function getExistingRepoNames(credentials: Credentials): Promise<APIResponse<RepoNamesSuccss>>;
80
+ export declare function getExistingRepoNames(credentials: Credentials): Promise<APIResponse<RepoNamesSuccess>>;
80
81
  interface AuthTokensResponse {
81
82
  scopes: string[];
82
83
  email: string;
@@ -90,4 +91,5 @@ interface AuthTokensResponse {
90
91
  };
91
92
  }
92
93
  export declare function getAuthDetails(credentials: Credentials): Promise<APIResponse<AuthTokensResponse>>;
94
+ export declare function mergeUsageProps(newProps: ComponentProps, currentProps?: ComponentProps): any;
93
95
  export {};
@@ -71,13 +71,35 @@ async function request(path, credentials, init) {
71
71
  }
72
72
  return await response.json();
73
73
  }
74
+ export function mergeUsageProps(newProps, currentProps) {
75
+ if (!currentProps)
76
+ return newProps;
77
+ if (!newProps)
78
+ return currentProps;
79
+ const props = Array.from(new Set([...Object.keys(currentProps), ...Object.keys(newProps)]));
80
+ const mergedProps = {};
81
+ props.forEach((prop) => {
82
+ const existingProp = currentProps[prop] ?? {};
83
+ const newProp = newProps[prop];
84
+ const mergedValues = [
85
+ ...(existingProp?.values ?? []),
86
+ ...(newProp?.values ?? []),
87
+ ];
88
+ mergedProps[prop] = {
89
+ type: Array.from(new Set([...(existingProp.type ?? []), ...(newProp?.type ?? [])])),
90
+ count: mergedValues.length,
91
+ values: mergedValues,
92
+ };
93
+ });
94
+ return mergedProps;
95
+ }
74
96
  /**
75
97
  * Transform usage map grouped by file into usage map grouped by component
76
98
  */
77
99
  function transformUsageByName(usage) {
78
100
  const transformedUsageMap = new Map();
79
101
  for (const [file, rawUsage] of usage) {
80
- for (const { count, package: packageName, name } of rawUsage) {
102
+ for (const { count, package: packageName, name, props } of rawUsage) {
81
103
  const key = `package:${packageName} name:${name}`;
82
104
  const currentValue = transformedUsageMap.get(key);
83
105
  const newFileList = [...(currentValue?.files ?? []), file]; // Add file to list
@@ -86,6 +108,7 @@ function transformUsageByName(usage) {
86
108
  files: Array.from(new Set(newFileList)), // Ensure unique file paths
87
109
  count: count + (currentValue?.count ?? 0),
88
110
  package: packageName,
111
+ props: mergeUsageProps(props, currentValue?.props ?? {}),
89
112
  });
90
113
  }
91
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroheight/adoption-cli",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "license": "ISC",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {