@zeroheight/adoption-cli 2.0.2 → 2.2.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.2.0](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/2.2.0) - 8th November 2024
4
+
5
+ - Include export names in package file as part of component analyzing
6
+
7
+ ## [2.1.0](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/2.1.0) - 8th November 2024
8
+
9
+ - Start collecting component property usage data as part of component analyzing
10
+
3
11
  ## [2.0.2](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/2.0.2) - 29th October 2024
4
12
 
5
13
  - 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.2.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
@@ -8,3 +8,7 @@ export declare function getPackageInfo(): Promise<{
8
8
  files: PackageFile[];
9
9
  error: string | null;
10
10
  }>;
11
+ /**
12
+ * Transform exports object into fully qualified aliases
13
+ */
14
+ export declare function getAliasesFromExports(packageName: string, exports?: PackageFile["exports"]): string[];
@@ -21,10 +21,12 @@ export async function getPackageInfo() {
21
21
  try {
22
22
  const packageFiles = await Promise.all(files.map(async (file) => {
23
23
  const fileContents = await readFile(file, "utf-8");
24
+ const parsedPackage = JSON.parse(fileContents);
24
25
  return {
25
- name: JSON.parse(fileContents).name,
26
+ name: parsedPackage.name,
26
27
  path: `.${files[0]?.split(base).pop()}`,
27
- version: JSON.parse(fileContents).version,
28
+ version: parsedPackage.version,
29
+ exports: parsedPackage.exports,
28
30
  };
29
31
  }));
30
32
  return {
@@ -39,3 +41,13 @@ export async function getPackageInfo() {
39
41
  };
40
42
  }
41
43
  }
44
+ /**
45
+ * Transform exports object into fully qualified aliases
46
+ */
47
+ export function getAliasesFromExports(packageName, exports) {
48
+ if (!exports)
49
+ return [];
50
+ return Object.keys(exports)
51
+ .map((path) => joinPath(packageName, path))
52
+ .filter((name) => name !== packageName);
53
+ }
@@ -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 {
@@ -38,7 +39,7 @@ interface PackageDetailsSuccessResponse {
38
39
  updated_at: string;
39
40
  };
40
41
  }
41
- export declare function submitPackageDetails(name: string, path: string, version: string, credentials: Credentials): Promise<APIResponse<PackageDetailsSuccessResponse, {}, {}>>;
42
+ export declare function submitPackageDetails(name: string, path: string, version: string, aliases: string[], credentials: Credentials): Promise<APIResponse<PackageDetailsSuccessResponse, {}, {}>>;
42
43
  interface MonitoredRepoDetailsSuccess {
43
44
  monitored_repository: {
44
45
  id: number;
@@ -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 {};
@@ -13,11 +13,12 @@ export function getZeroheightURL() {
13
13
  return new URL("https://zeroheight.com");
14
14
  }
15
15
  }
16
- export async function submitPackageDetails(name, path, version, credentials) {
16
+ export async function submitPackageDetails(name, path, version, aliases, credentials) {
17
17
  return post("/design_system_packages", {
18
18
  name,
19
19
  path,
20
20
  latest_version: version,
21
+ aliases,
21
22
  }, credentials);
22
23
  }
23
24
  export async function submitMonitoredRepoDetails(name, version, lockfilePath, packages, credentials) {
@@ -71,13 +72,35 @@ async function request(path, credentials, init) {
71
72
  }
72
73
  return await response.json();
73
74
  }
75
+ export function mergeUsageProps(newProps, currentProps) {
76
+ if (!currentProps)
77
+ return newProps;
78
+ if (!newProps)
79
+ return currentProps;
80
+ const props = Array.from(new Set([...Object.keys(currentProps), ...Object.keys(newProps)]));
81
+ const mergedProps = {};
82
+ props.forEach((prop) => {
83
+ const existingProp = currentProps[prop] ?? {};
84
+ const newProp = newProps[prop];
85
+ const mergedValues = [
86
+ ...(existingProp?.values ?? []),
87
+ ...(newProp?.values ?? []),
88
+ ];
89
+ mergedProps[prop] = {
90
+ type: Array.from(new Set([...(existingProp.type ?? []), ...(newProp?.type ?? [])])),
91
+ count: mergedValues.length,
92
+ values: mergedValues,
93
+ };
94
+ });
95
+ return mergedProps;
96
+ }
74
97
  /**
75
98
  * Transform usage map grouped by file into usage map grouped by component
76
99
  */
77
100
  function transformUsageByName(usage) {
78
101
  const transformedUsageMap = new Map();
79
102
  for (const [file, rawUsage] of usage) {
80
- for (const { count, package: packageName, name } of rawUsage) {
103
+ for (const { count, package: packageName, name, props } of rawUsage) {
81
104
  const key = `package:${packageName} name:${name}`;
82
105
  const currentValue = transformedUsageMap.get(key);
83
106
  const newFileList = [...(currentValue?.files ?? []), file]; // Add file to list
@@ -86,6 +109,7 @@ function transformUsageByName(usage) {
86
109
  files: Array.from(new Set(newFileList)), // Ensure unique file paths
87
110
  count: count + (currentValue?.count ?? 0),
88
111
  package: packageName,
112
+ props: mergeUsageProps(props, currentValue?.props ?? {}),
89
113
  });
90
114
  }
91
115
  }
@@ -5,4 +5,9 @@ export interface PackageFile {
5
5
  name: string;
6
6
  path: string;
7
7
  version: string;
8
+ exports?: Record<string, Partial<{
9
+ import: string;
10
+ require: string;
11
+ types: string;
12
+ }> | string>;
8
13
  }
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { Newline, Text, useApp } from "ink";
3
3
  import Spinner from "ink-spinner";
4
4
  import { readConfig } from "../../common/config.js";
5
- import { getPackageInfo } from "../../commands/track-package.utils.js";
5
+ import { getAliasesFromExports, getPackageInfo, } from "../../commands/track-package.utils.js";
6
6
  import { ResponseStatus, submitPackageDetails } from "../../common/api.js";
7
7
  var Step;
8
8
  (function (Step) {
@@ -24,11 +24,12 @@ export default function NonInteractiveTrackPackage() {
24
24
  const { files, error } = await getPackageInfo();
25
25
  try {
26
26
  if (files.length === 1) {
27
- const { name, path, version } = files[0];
27
+ const { name, path, version, exports } = files[0];
28
28
  setPackageName(name);
29
29
  setPackageVersion(version);
30
30
  setCurrentStep(Step.SINGLE_PACKAGE_FOUND);
31
- const response = await submitPackageDetails(name, path, version, {
31
+ const aliases = getAliasesFromExports(name, exports);
32
+ const response = await submitPackageDetails(name, path, version, aliases, {
32
33
  token: config.token,
33
34
  client: config.client,
34
35
  });
@@ -46,7 +47,8 @@ export default function NonInteractiveTrackPackage() {
46
47
  setPackageFiles(files);
47
48
  setCurrentStep(Step.MULTIPLE_PACKAGES_FOUND);
48
49
  await Promise.all(files.map(async (pack) => {
49
- await submitPackageDetails(pack.name, pack.path, pack.version, {
50
+ const aliases = getAliasesFromExports(pack.name, pack.exports);
51
+ await submitPackageDetails(pack.name, pack.path, pack.version, aliases, {
50
52
  token: config.token,
51
53
  client: config.client,
52
54
  });
@@ -5,7 +5,7 @@ import SelectInput from "ink-select-input";
5
5
  import Spinner from "ink-spinner";
6
6
  import ConfirmInput from "../ui/confirm-input.js";
7
7
  import { readConfig } from "../../common/config.js";
8
- import { getPackageInfo } from "../../commands/track-package.utils.js";
8
+ import { getAliasesFromExports, getPackageInfo, } from "../../commands/track-package.utils.js";
9
9
  import { submitPackageDetails } from "../../common/api.js";
10
10
  var Step;
11
11
  (function (Step) {
@@ -28,12 +28,13 @@ export default function TrackPackage() {
28
28
  const [packageName, setPackageName] = React.useState(null);
29
29
  const [packagePath, setPackagePath] = React.useState(null);
30
30
  const [packageVersion, setPackageVersion] = React.useState(null);
31
+ const [packageAliases, setPackageAliases] = React.useState([]);
31
32
  const [shouldSend, setShouldSend] = React.useState("");
32
33
  const [shouldMultiSelect, setShouldMultiSelect] = React.useState("");
33
34
  const [packageSelection, setPackageSelection] = React.useState([]);
34
35
  const [selectedFileLabels, setSelectedFileLabels] = React.useState([]);
35
- async function submitPackage(name, path, version) {
36
- await submitPackageDetails(name, path, version, credentials);
36
+ async function submitPackage(name, path, version, aliases) {
37
+ await submitPackageDetails(name, path, version, aliases, credentials);
37
38
  }
38
39
  async function sendData() {
39
40
  setCurrentStep(Step.SENDING_DETAILS);
@@ -42,18 +43,20 @@ export default function TrackPackage() {
42
43
  const packagesToSend = packageFiles.filter((p) => selectedFileLabels.includes(`${p.name}@${p.version}`));
43
44
  if (packagesToSend.length > 0) {
44
45
  await Promise.all(packagesToSend.map(async (pack) => {
45
- submitPackage(pack.name, pack.path, pack.version);
46
+ const aliases = getAliasesFromExports(pack.name, pack.exports);
47
+ submitPackage(pack.name, pack.path, pack.version, aliases);
46
48
  }));
47
49
  }
48
50
  else {
49
51
  await Promise.all(packageFiles.map(async (pack) => {
50
- submitPackage(pack.name, pack.path, pack.version);
52
+ const aliases = getAliasesFromExports(pack.name, pack.exports);
53
+ submitPackage(pack.name, pack.path, pack.version, aliases);
51
54
  }));
52
55
  }
53
56
  setCurrentStep(Step.COMPLETE);
54
57
  }
55
58
  else {
56
- await submitPackage(packageName, packagePath, packageVersion);
59
+ await submitPackage(packageName, packagePath, packageVersion, packageAliases);
57
60
  setCurrentStep(Step.COMPLETE);
58
61
  }
59
62
  }
@@ -101,10 +104,11 @@ export default function TrackPackage() {
101
104
  }
102
105
  const { files, error } = await getPackageInfo();
103
106
  if (files.length === 1) {
104
- const { name, path, version } = files[0];
107
+ const { name, path, version, exports } = files[0];
105
108
  setPackageName(name);
106
109
  setPackagePath(path);
107
110
  setPackageVersion(version);
111
+ setPackageAliases(getAliasesFromExports(name, exports));
108
112
  setCurrentStep(Step.SINGLE_PACKAGE_FOUND);
109
113
  }
110
114
  else if (files.length > 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroheight/adoption-cli",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "license": "ISC",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {