@zeroheight/adoption-cli 2.0.2 → 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,9 @@
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
+
3
7
  ## [2.0.2](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/2.0.2) - 29th October 2024
4
8
 
5
9
  - Update readme
@@ -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.2")
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.2",
3
+ "version": "2.1.0",
4
4
  "license": "ISC",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {