@zeroheight/adoption-cli 0.2.13 → 0.3.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,14 @@
1
1
  # Release notes
2
2
 
3
+ ## [0.3.1](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/0.3.1) - 30th July 2024
4
+
5
+ - Condense analyze output to be easier to parse
6
+ - Output parsing errors into log file
7
+
8
+ ## [0.3.0](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/0.3.0) - 29th July 2024
9
+
10
+ - Replace acorn parser with oxc-parser
11
+
3
12
  ## [0.2.13](https://www.npmjs.com/package/@zeroheight/adoption-cli/v/0.2.13) - 25th July 2024
4
13
 
5
14
  - Fix ws dependency security issue — https://github.com/advisories/GHSA-3h5v-q93c-6h6q
@@ -1,4 +1,8 @@
1
- import type { Node } from "acorn";
1
+ import type { Node, Program, Identifier } from "acorn";
2
+ export interface PlainIdentifier extends Node {
3
+ type: "Identifier";
4
+ local: Identifier;
5
+ }
2
6
  export type VisitorState = {
3
7
  components: Map<string, number>;
4
8
  imports: Map<string, {
@@ -11,4 +15,4 @@ export type RawUsage = {
11
15
  count: number;
12
16
  package: string;
13
17
  };
14
- export declare function analyze(ast: Node): RawUsage[];
18
+ export declare function analyze(ast: Program): RawUsage[];
@@ -23,27 +23,98 @@ export function analyze(ast) {
23
23
  components: new Map(),
24
24
  imports: new Map(),
25
25
  };
26
- const visitors = {
27
- JSXElement(node, state, _nodePath) {
26
+ const visitorFunctions = {
27
+ CatchClause() { },
28
+ CatchParameter() { },
29
+ ComputedMemberExpression() { },
30
+ JSXText() { },
31
+ NullLiteral() { },
32
+ NumericLiteral() { },
33
+ BooleanLiteral() { },
34
+ ObjectExpression() { },
35
+ ObjectProperty() { },
36
+ StringLiteral() { },
37
+ TSAsExpression() { },
38
+ TSEnumDeclaration() { },
39
+ TSInterfaceDeclaration() { },
40
+ TSTypeAliasDeclaration() { },
41
+ TSModuleDeclaration() { },
42
+ TSSatisfiesExpression() { },
43
+ StaticMemberExpression() { },
44
+ RegExpLiteral() { },
45
+ TSNonNullExpression() { },
46
+ JSXEmptyExpression() { },
47
+ JSXFragment(node, state) {
48
+ node.children.forEach((child) => walk.recursive(child, state, visitorFunctions, walk.base));
49
+ },
50
+ IfStatement(node, state) {
51
+ walk.recursive(node.consequent, state, visitorFunctions, walk.base);
52
+ if (node.alternate) {
53
+ walk.recursive(node.alternate, state, visitorFunctions, walk.base);
54
+ }
55
+ },
56
+ VariableDeclaration(node, state) {
57
+ node.declarations.forEach((child) => {
58
+ walk.recursive(child, state, visitorFunctions, walk.base);
59
+ });
60
+ },
61
+ ArrowFunctionExpression(node, state) {
62
+ if (node.body.type === "FunctionBody") {
63
+ walk.recursive(node.body, state, visitorFunctions, walk.base);
64
+ }
65
+ },
66
+ ParenthesizedExpression(node, state) {
67
+ walk.recursive(node.expression, state, visitorFunctions, walk.base);
68
+ },
69
+ ReturnStatement(node, state) {
70
+ if (node.argument) {
71
+ walk.recursive(node.argument, state, visitorFunctions, walk.base);
72
+ }
73
+ },
74
+ FunctionBody(node, state) {
75
+ node.statements.forEach((child) => walk.recursive(child, state, visitorFunctions, walk.base));
76
+ },
77
+ Program(node, state) {
78
+ node.body.forEach((child) => {
79
+ walk.recursive(child, state, visitorFunctions, walk.base);
80
+ });
81
+ },
82
+ JSXElement(node, state) {
28
83
  const el = node.openingElement;
29
84
  const elName = el.name.name;
30
- if (!elName)
31
- return;
32
85
  // Ignore html tags e.g. div, h1, span
33
- if (elName[0] === elName[0]?.toLocaleLowerCase())
34
- return;
35
- state.components.set(elName, (state.components.get(elName) ?? 0) + 1);
86
+ if (elName?.[0] !== elName?.[0]?.toLocaleLowerCase()) {
87
+ state.components.set(elName, (state.components.get(elName) ?? 0) + 1);
88
+ }
89
+ node.children.forEach((child) => {
90
+ walk.recursive(child, state, visitorFunctions, walk.base);
91
+ });
92
+ },
93
+ CallExpression(node, state) {
94
+ node.arguments.forEach((child) => walk.recursive(child, state, visitorFunctions, walk.base));
95
+ },
96
+ ExportDefaultDeclaration(node, state) {
97
+ if (node.declaration.type === "FunctionDeclaration") {
98
+ const { statements } = node.declaration.body;
99
+ statements.forEach((child) => {
100
+ walk.recursive(child, state, visitorFunctions, walk.base);
101
+ });
102
+ }
103
+ },
104
+ JSXExpressionContainer(node, state) {
105
+ walk.recursive(node.expression, state, visitorFunctions, walk.base);
36
106
  },
37
- ImportDeclaration(node, state, _nodePath) {
38
- const packageName = node.source.value;
39
- node.specifiers.forEach((specifier) => {
107
+ ImportDeclaration(node, state) {
108
+ const packageName = node.source.value?.toString() ?? "";
109
+ node.specifiers?.forEach((specifier) => {
40
110
  // not handling namespace imports as we can't determine the actual name
41
111
  // e.g. import * as MyIconLibrary from 'my-icon-library'
42
112
  if (specifier.type === "ImportNamespaceSpecifier") {
43
113
  return;
44
114
  }
45
115
  let name = specifier.local.name;
46
- if (specifier.type === "ImportDefaultSpecifier") {
116
+ if (specifier.type === "ImportDefaultSpecifier" ||
117
+ specifier.type === "Identifier") {
47
118
  // handles the following cases:
48
119
  // import Icon from 'my-icon-library'
49
120
  name = specifier.local.name;
@@ -63,23 +134,10 @@ export function analyze(ast) {
63
134
  });
64
135
  });
65
136
  },
66
- };
67
- const base = {
68
- ...walk.base,
69
- TSInterfaceDeclaration() { },
70
- TSModuleDeclaration() { },
71
- TSAsExpression() { },
72
- TSDeclareFunction() { },
73
- TSTypeAliasDeclaration() { },
74
- TSNonNullExpression() { },
75
- TSEnumDeclaration() { },
76
- TSParameterProperty() { },
77
- TSImportEqualsDeclaration() { },
78
- TSInstantiationExpression() { },
79
- TSDeclareMethod() { },
137
+ // FIXME: Not the best typing but there are additional types not in acorn
80
138
  };
81
139
  try {
82
- walk.ancestor(ast, visitors, base, visitorState);
140
+ walk.recursive(ast, visitorState, visitorFunctions, walk.base);
83
141
  }
84
142
  catch (e) {
85
143
  // We don't want to log the error to the users - maybe could add some actually logging in the future
@@ -1,11 +1,10 @@
1
- import * as acorn from "acorn";
2
1
  /**
3
2
  * Parse code and return AST
4
3
  *
5
4
  * This extends the acorn parser with support for TypeScript and JSX
6
5
  *
7
- * @param code The string containing JS/TS
8
- * @param version a version of ECMA Script to use, defaults to latest
6
+ * @param code The string containing JS?X/TS?X
7
+ * @param sourceFilename a filename to help with inference
9
8
  * @returns Parsed AST
10
9
  */
11
- export declare function parse(code: string, version?: acorn.ecmaVersion): acorn.Program;
10
+ export declare function parse(code: string, sourceFilename?: string): any;
@@ -1,23 +1,16 @@
1
- import * as acorn from "acorn";
2
- import { tsPlugin } from "acorn-typescript";
3
- import * as walk from "acorn-walk";
4
- import { extend } from "acorn-jsx-walk";
1
+ import oxc from "oxc-parser";
5
2
  /**
6
3
  * Parse code and return AST
7
4
  *
8
5
  * This extends the acorn parser with support for TypeScript and JSX
9
6
  *
10
- * @param code The string containing JS/TS
11
- * @param version a version of ECMA Script to use, defaults to latest
7
+ * @param code The string containing JS?X/TS?X
8
+ * @param sourceFilename a filename to help with inference
12
9
  * @returns Parsed AST
13
10
  */
14
- export function parse(code, version = "latest") {
15
- const typescriptPlugin = tsPlugin();
16
- extend(walk.base);
17
- const JSXParser = acorn.Parser.extend(typescriptPlugin);
18
- return JSXParser.parse(code, {
19
- ecmaVersion: version,
20
- sourceType: "module",
21
- locations: true,
22
- });
11
+ export function parse(code, sourceFilename) {
12
+ const result = oxc.parseSync(code, { sourceFilename, sourceType: "module" });
13
+ // TODO: Surface these errors to help in debugging
14
+ // console.error(result.errors)
15
+ return JSON.parse(result.program);
23
16
  }
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ const { output, cleanup } = render(React.createElement(HelpInfo, null));
10
10
  program
11
11
  .name("zh-adoption")
12
12
  .description("CLI for measuring design system usage usage in your products")
13
- .version("0.2.13")
13
+ .version("0.3.1")
14
14
  .addHelpText("before", output)
15
15
  .addCommand(analyzeCommand())
16
16
  .addCommand(authCommand());
@@ -15,6 +15,6 @@ export interface ComponentUsageRecord {
15
15
  */
16
16
  export declare function findFiles(base: string, extensions: string, ignorePattern: string): Promise<string[]>;
17
17
  export declare function analyzeFiles(extensions: string, ignorePattern: string): Promise<{
18
- errors: never[];
18
+ errorFile: string | null;
19
19
  usage: RawUsageMap;
20
20
  }>;
@@ -51,18 +51,27 @@ export async function analyzeFiles(extensions, ignorePattern) {
51
51
  if (files.length === 0) {
52
52
  throw new Error("Can't find any relevant files");
53
53
  }
54
+ const parseErrors = [];
54
55
  const usageMap = new Map();
55
56
  for (const file of files) {
56
57
  try {
57
58
  const fileContents = await readFile(file, "utf-8");
58
- const ast = parse(fileContents);
59
+ const ast = parse(fileContents, file);
59
60
  const usage = analyze(ast);
60
61
  if (usage.length > 0) {
61
62
  const relativePath = file.slice(process.cwd().length);
62
63
  usageMap.set(relativePath, usage);
63
64
  }
64
65
  }
65
- catch { }
66
+ catch {
67
+ parseErrors.push(`Can't parse file ${file}`);
68
+ }
69
+ }
70
+ const errorFile = `/tmp/zh-adoption-analyze-errors-${Date.now()}`;
71
+ if (parseErrors.length > 0) {
72
+ const file = fs.createWriteStream(errorFile);
73
+ parseErrors.forEach((err) => { file.write(err + '\n'); });
74
+ file.end();
66
75
  }
67
- return { errors: [], usage: usageMap };
76
+ return { errorFile: parseErrors.length > 0 ? errorFile : null, usage: usageMap };
68
77
  }
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { RawUsageMap } from "../../commands/analyze.js";
3
3
  export interface AnalyzeProps {
4
4
  onAnalyzeFiles: () => Promise<{
5
- errors: string[];
5
+ errorFile: string | null;
6
6
  usage: RawUsageMap;
7
7
  }>;
8
8
  dryRun: boolean;
@@ -8,19 +8,23 @@ import Link from "ink-link";
8
8
  import NoCredentialsOnboarding from "../auth/no-credentials-onboarding.js";
9
9
  import { getZeroheightURL, submitUsageData } from "../../common/api.js";
10
10
  import RepoNamePrompt from "../repo-name-prompt.js";
11
+ import ConfirmInput from "../ui/confirm-input.js";
11
12
  export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
12
13
  const { exit } = useApp();
13
14
  const [usageResult, setUsageResult] = React.useState(null);
14
15
  const [isSendingData, setIsSendingData] = React.useState(false);
15
16
  const [errorList, setErrorList] = React.useState([]);
17
+ const [errorFileLocation, setErrorFileLocation] = React.useState(null);
16
18
  const [credentials, setCredentials] = React.useState(null);
17
19
  const [resourceURL, setResourceURL] = React.useState(null);
18
20
  const [repo, setRepo] = React.useState(repoName);
19
21
  const [promptRepo, setPromptRepo] = React.useState(repoName === undefined);
22
+ const [showContinue, setShowContinue] = React.useState(false);
23
+ const [showMoreResults, setShowMoreResults] = React.useState("");
20
24
  React.useEffect(() => {
21
25
  onAnalyzeFiles()
22
- .then(({ errors, usage }) => {
23
- setErrorList((s) => [...s, ...errors]);
26
+ .then(({ errorFile, usage }) => {
27
+ setErrorFileLocation(errorFile);
24
28
  setUsageResult(usage);
25
29
  })
26
30
  .catch((e) => setErrorList((s) => [...s, e]));
@@ -70,6 +74,24 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
70
74
  });
71
75
  setPromptRepo(false);
72
76
  }
77
+ function handleShowMore(showMore) {
78
+ if (showMore) {
79
+ setShowMoreResults('true');
80
+ }
81
+ else {
82
+ setShowContinue(true);
83
+ }
84
+ }
85
+ function calculateNumberOfComponents() {
86
+ if (usageResult) {
87
+ const components = Array.from(usageResult.entries()).flatMap((comps) => comps[1]);
88
+ const componentCount = components.reduce((prev, next) => {
89
+ return prev + next.count;
90
+ }, 0);
91
+ return componentCount;
92
+ }
93
+ return 0;
94
+ }
73
95
  if (errorList.length > 0) {
74
96
  return (React.createElement(Box, { flexDirection: "column" },
75
97
  React.createElement(Text, { color: "red" }, "Error:"),
@@ -117,7 +139,27 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
117
139
  " ",
118
140
  React.createElement(Link, { url: resourceURL.toString() }, "usage data on zeroheight"))));
119
141
  }
120
- if (usageResult && !promptRepo) {
142
+ if (usageResult && !promptRepo && !(showMoreResults === 'true' || showContinue)) {
143
+ return (React.createElement(Box, { flexDirection: "column" },
144
+ React.createElement(Text, { color: "green" },
145
+ calculateNumberOfComponents(),
146
+ " components found"),
147
+ errorFileLocation && (React.createElement(React.Fragment, null,
148
+ React.createElement(Newline, null),
149
+ React.createElement(Box, null,
150
+ React.createElement(Text, { color: "red" }, "Error:"),
151
+ React.createElement(Text, null,
152
+ ' ',
153
+ "Some files could not be parsed. Errors can be found at ",
154
+ React.createElement(Link, { url: errorFileLocation }, errorFileLocation))))),
155
+ React.createElement(Box, null,
156
+ React.createElement(Text, null,
157
+ "Show more details?",
158
+ " ",
159
+ React.createElement(Text, { dimColor: true }, "(y/N)")),
160
+ React.createElement(ConfirmInput, { isChecked: false, value: showMoreResults, onChange: setShowMoreResults, onSubmit: handleShowMore }))));
161
+ }
162
+ if (usageResult && !promptRepo && showMoreResults === 'true') {
121
163
  return (React.createElement(Box, { flexDirection: "column" },
122
164
  React.createElement(UsageTable, { usage: usageResult }),
123
165
  dryRun && (React.createElement(React.Fragment, null,
@@ -129,6 +171,17 @@ export default function Analyze({ onAnalyzeFiles, dryRun, repoName, }) {
129
171
  React.createElement(Newline, null))),
130
172
  !dryRun && React.createElement(ContinuePrompt, { onContinue: handleContinue })));
131
173
  }
174
+ if (usageResult && !promptRepo && showContinue) {
175
+ return (React.createElement(Box, { flexDirection: "column" },
176
+ dryRun && (React.createElement(React.Fragment, null,
177
+ React.createElement(Newline, null),
178
+ React.createElement(Text, null,
179
+ "Nothing sent to zeroheight, if you want to track component usage data then remove the ",
180
+ React.createElement(Text, { italic: true }, "--dry-run"),
181
+ " option."),
182
+ React.createElement(Newline, null))),
183
+ !dryRun && React.createElement(ContinuePrompt, { onContinue: handleContinue })));
184
+ }
132
185
  if (promptRepo) {
133
186
  return (React.createElement(RepoNamePrompt, { credentials: credentials, onRepoNameSelected: handleOnRepoNameSelected }));
134
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroheight/adoption-cli",
3
- "version": "0.2.13",
3
+ "version": "0.3.1",
4
4
  "license": "ISC",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
@@ -26,8 +26,6 @@
26
26
  ],
27
27
  "dependencies": {
28
28
  "acorn": "^8.11.3",
29
- "acorn-jsx-walk": "^2.0.0",
30
- "acorn-typescript": "^1.4.13",
31
29
  "acorn-walk": "^8.3.2",
32
30
  "chalk": "^5.2.0",
33
31
  "commander": "^12.0.0",
@@ -39,6 +37,7 @@
39
37
  "ink-select-input": "^5.0.0",
40
38
  "ink-spinner": "^5.0.0",
41
39
  "ink-text-input": "^5.0.1",
40
+ "oxc-parser": "^0.22.0",
42
41
  "react": "^18.2.0",
43
42
  "yn": "^5.0.0"
44
43
  },