@zeroheight/adoption-cli 0.2.8 → 0.2.10

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.
@@ -0,0 +1,3 @@
1
+ import { Root } from "hast";
2
+ import { RawUsage } from "../analyze.js";
3
+ export declare function analyze(tree: Root): RawUsage[];
@@ -0,0 +1,57 @@
1
+ import { visit } from "unist-util-visit";
2
+ import { visit as visitEstree } from "estree-util-visit";
3
+ import { transformVisitorStateToRawUsage, } from "../analyze.js";
4
+ import { parse as parseTSJS } from "../ts-js/parser.js";
5
+ export function analyze(tree) {
6
+ /**
7
+ * TODO: Populating imports means parsing JS files directly and finding:
8
+ * window.customElements.define('my-custom-element', MyCustomElement);
9
+ */
10
+ const state = {
11
+ components: new Map(),
12
+ imports: new Map(),
13
+ };
14
+ const rawPaths = [];
15
+ visit(tree, (node) => {
16
+ if (node.type !== "element")
17
+ return;
18
+ if (node.tagName === "script") {
19
+ rawPaths.push(...analyzeScript(node));
20
+ return;
21
+ }
22
+ const isCustomElement = node.tagName.includes("-");
23
+ if (!isCustomElement)
24
+ return;
25
+ state.components.set(node.tagName, (state.components.get(node.tagName) ?? 0) + 1);
26
+ // Dumb way of guessing import name
27
+ rawPaths.forEach((p) => {
28
+ if (p.includes(node.tagName)) {
29
+ state.imports.set(node.tagName, {
30
+ name: node.tagName,
31
+ package: p,
32
+ });
33
+ }
34
+ });
35
+ });
36
+ return transformVisitorStateToRawUsage(state);
37
+ }
38
+ function analyzeScript(node) {
39
+ const importPaths = [];
40
+ const rawCode = node.children
41
+ .filter((child) => child.type === "text")
42
+ .map((child) => child.value)
43
+ .join("\n");
44
+ const tree = parseTSJS(rawCode, {
45
+ type: "ts-js",
46
+ version: "latest",
47
+ });
48
+ visitEstree(tree, (jsNode) => {
49
+ if (jsNode.type !== "ImportDeclaration")
50
+ return;
51
+ const rawPath = jsNode.source.value;
52
+ if (typeof rawPath === "string") {
53
+ importPaths.push(rawPath);
54
+ }
55
+ });
56
+ return importPaths;
57
+ }
@@ -0,0 +1,4 @@
1
+ export interface HTMLOptions {
2
+ type: "html";
3
+ }
4
+ export declare function parse(code: string, _options?: HTMLOptions): import("hast").Root;
@@ -0,0 +1,6 @@
1
+ import { unified } from "unified";
2
+ import rehypeParse from "rehype-parse";
3
+ export function parse(code, _options) {
4
+ const tree = unified().use(rehypeParse, { fragment: true }).parse(code);
5
+ return tree;
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { Node } from "acorn";
2
+ export declare function analyze(ast: Node): import("../analyze.js").RawUsage[];
@@ -0,0 +1,73 @@
1
+ import * as walk from "acorn-walk";
2
+ import { transformVisitorStateToRawUsage } from "../analyze.js";
3
+ export function analyze(ast) {
4
+ const visitorState = {
5
+ components: new Map(),
6
+ imports: new Map(),
7
+ };
8
+ const visitors = {
9
+ JSXElement(node, state, _nodePath) {
10
+ const el = node.openingElement;
11
+ const elName = el.name.name;
12
+ if (!elName)
13
+ return;
14
+ // Ignore html tags e.g. div, h1, span
15
+ if (elName[0] === elName[0]?.toLocaleLowerCase())
16
+ return;
17
+ state.components.set(elName, (state.components.get(elName) ?? 0) + 1);
18
+ },
19
+ ImportDeclaration(node, state, _nodePath) {
20
+ const packageName = node.source.value;
21
+ node.specifiers.forEach((specifier) => {
22
+ // not handling namespace imports as we can't determine the actual name
23
+ // e.g. import * as MyIconLibrary from 'my-icon-library'
24
+ if (specifier.type === "ImportNamespaceSpecifier") {
25
+ return;
26
+ }
27
+ let name = specifier.local.name;
28
+ if (specifier.type === "ImportDefaultSpecifier") {
29
+ // handles the following cases:
30
+ // import Icon from 'my-icon-library'
31
+ name = specifier.local.name;
32
+ }
33
+ else if (specifier.type === "ImportSpecifier") {
34
+ // handles the following cases:
35
+ // import { Icon } from 'my-icon-library'
36
+ // import { Icon as MyIcon } from 'my-icon-library'
37
+ name =
38
+ specifier.imported.type === "Literal"
39
+ ? specifier.imported.value
40
+ : specifier.imported.name;
41
+ }
42
+ state.imports.set(specifier.local.name, {
43
+ package: packageName,
44
+ name,
45
+ });
46
+ });
47
+ },
48
+ };
49
+ const base = {
50
+ ...walk.base,
51
+ TSInterfaceDeclaration() { },
52
+ TSModuleDeclaration() { },
53
+ TSAsExpression() { },
54
+ TSDeclareFunction() { },
55
+ TSTypeAliasDeclaration() { },
56
+ TSNonNullExpression() { },
57
+ TSEnumDeclaration() { },
58
+ TSParameterProperty() { },
59
+ TSImportEqualsDeclaration() { },
60
+ TSInstantiationExpression() { },
61
+ TSDeclareMethod() { },
62
+ };
63
+ try {
64
+ walk.ancestor(ast, visitors, base, visitorState);
65
+ }
66
+ catch (e) {
67
+ // We don't want to log the error to the users - maybe could add some actually logging in the future
68
+ // console.log("error", e);
69
+ }
70
+ finally {
71
+ return transformVisitorStateToRawUsage(visitorState);
72
+ }
73
+ }
@@ -0,0 +1,6 @@
1
+ import * as acorn from "acorn";
2
+ export interface TSJSOptions {
3
+ type: "ts-js";
4
+ version?: acorn.ecmaVersion;
5
+ }
6
+ export declare function parse(code: string, options?: TSJSOptions): acorn.Program;
@@ -0,0 +1,14 @@
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";
5
+ export function parse(code, options) {
6
+ const typescriptPlugin = tsPlugin();
7
+ extend(walk.base);
8
+ const JSXParser = acorn.Parser.extend(typescriptPlugin);
9
+ return JSXParser.parse(code, {
10
+ ecmaVersion: options?.version ?? "latest",
11
+ sourceType: "module",
12
+ locations: true,
13
+ });
14
+ }
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.7")
13
+ .version("0.2.10")
14
14
  .addHelpText("before", output)
15
15
  .addCommand(analyzeCommand())
16
16
  .addCommand(authCommand());
@@ -1,41 +1,9 @@
1
1
  import * as React from "react";
2
2
  import { Command } from "commander";
3
- import { Box, Text, render } from "ink";
4
- import CredentialsAlreadyExists from "../components/auth/credentials-already-exists.js";
5
- import CredentialsPreview from "../components/auth/credentials-preview.js";
6
- import NoCredentialsOnboarding from "../components/auth/no-credentials-onboarding.js";
7
- import { configPath, readConfig, writeConfig } from "../common/config.js";
3
+ import { render } from "ink";
4
+ import Auth from "../components/auth/auth.js";
8
5
  export async function authAction(options, renderOptions) {
9
- const existingConfig = await readConfig();
10
- if (existingConfig) {
11
- const { waitUntilExit } = render(React.createElement(CredentialsAlreadyExists, { existingConfig: existingConfig, token: options.token, client: options.client, configPath: configPath(), onOverwriteCredentials: async () => {
12
- const newClient = options.client ?? existingConfig.client;
13
- const newToken = options.token ?? existingConfig.token;
14
- await writeConfig({
15
- client: newClient,
16
- token: newToken,
17
- });
18
- render(React.createElement(Box, { flexDirection: "column" },
19
- React.createElement(Text, { color: "green" }, "Updated credentials"),
20
- React.createElement(CredentialsPreview, { client: newClient, token: newToken })));
21
- } }), renderOptions);
22
- await waitUntilExit();
23
- }
24
- else {
25
- const { waitUntilExit, rerender } = render(React.createElement(NoCredentialsOnboarding, { configPath: configPath(), onSaveCredentials: async (persist, newClient, newToken) => {
26
- if (persist) {
27
- await writeConfig({
28
- client: newClient,
29
- token: newToken,
30
- });
31
- rerender(React.createElement(Box, { flexDirection: "column" },
32
- React.createElement(Text, { color: "green" }, "Updated credentials"),
33
- React.createElement(CredentialsPreview, { client: newClient, token: newToken })));
34
- }
35
- } }));
36
- await waitUntilExit();
37
- }
38
- render(React.createElement(Text, { bold: true }, "Done"));
6
+ render(React.createElement(Auth, { token: options.token, client: options.client }), renderOptions);
39
7
  }
40
8
  export function authCommand() {
41
9
  const command = new Command();
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ interface AuthProps {
3
+ token?: string;
4
+ client?: string;
5
+ }
6
+ export default function Auth({ token, client }: AuthProps): React.JSX.Element;
7
+ export {};
@@ -0,0 +1,105 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ import ConfirmInput from "../ui/confirm-input.js";
4
+ import NoCredentialsOnboarding from "./no-credentials-onboarding.js";
5
+ import { configPath, readConfig, writeConfig, } from "../../common/config.js";
6
+ import CredentialsAlreadyExists from "./credentials-already-exists.js";
7
+ import CredentialsPreview from "./credentials-preview.js";
8
+ import Spinner from "ink-spinner";
9
+ var Step;
10
+ (function (Step) {
11
+ Step[Step["LOADING_CONFIG"] = 0] = "LOADING_CONFIG";
12
+ Step[Step["PROMPT_OVERWRITE"] = 1] = "PROMPT_OVERWRITE";
13
+ Step[Step["PROMPT_NEW_CREDENTIALS"] = 2] = "PROMPT_NEW_CREDENTIALS";
14
+ Step[Step["DONE_WRITE_FAIL"] = 3] = "DONE_WRITE_FAIL";
15
+ Step[Step["DONE_WRITE_SUCCESS"] = 4] = "DONE_WRITE_SUCCESS";
16
+ Step[Step["DONE_NO_CHANGE"] = 5] = "DONE_NO_CHANGE";
17
+ })(Step || (Step = {}));
18
+ export default function Auth({ token, client }) {
19
+ const [shouldOverwriteAnswer, setShouldOverwriteAnswer] = React.useState("");
20
+ const [existingConfig, setExistingConfig] = React.useState(null);
21
+ const [currentStep, setCurrentStep] = React.useState(Step.LOADING_CONFIG);
22
+ React.useEffect(() => {
23
+ readConfig().then((config) => {
24
+ setCurrentStep(Step.LOADING_CONFIG);
25
+ setExistingConfig(config);
26
+ if (!config) {
27
+ setCurrentStep(Step.PROMPT_NEW_CREDENTIALS);
28
+ }
29
+ else if ((config && token) || (config && client)) {
30
+ mergeAndWriteConfig(config, client, token);
31
+ }
32
+ else if (config) {
33
+ setCurrentStep(Step.PROMPT_OVERWRITE);
34
+ }
35
+ });
36
+ }, []);
37
+ async function mergeAndWriteConfig(oldConfig, newClient, newToken) {
38
+ const clientToWrite = newClient ?? oldConfig?.client;
39
+ const tokenToWrite = newToken ?? oldConfig?.token;
40
+ if (!clientToWrite || !tokenToWrite) {
41
+ setCurrentStep(Step.DONE_WRITE_FAIL);
42
+ return;
43
+ }
44
+ try {
45
+ const updatedConfig = {
46
+ client: clientToWrite,
47
+ token: tokenToWrite,
48
+ };
49
+ setCurrentStep(Step.LOADING_CONFIG);
50
+ setExistingConfig(updatedConfig);
51
+ await writeConfig(updatedConfig);
52
+ }
53
+ catch {
54
+ setCurrentStep(Step.DONE_WRITE_FAIL);
55
+ return;
56
+ }
57
+ setCurrentStep(Step.DONE_WRITE_SUCCESS);
58
+ }
59
+ async function handleSubmitOverwrite(override) {
60
+ if (override) {
61
+ setCurrentStep(Step.PROMPT_NEW_CREDENTIALS);
62
+ }
63
+ else {
64
+ setCurrentStep(Step.DONE_NO_CHANGE);
65
+ }
66
+ }
67
+ async function handleSaveCredentials(persist, newClient, newToken) {
68
+ if (persist) {
69
+ await mergeAndWriteConfig(existingConfig, newClient, newToken);
70
+ }
71
+ else {
72
+ setCurrentStep(Step.DONE_NO_CHANGE);
73
+ }
74
+ }
75
+ switch (currentStep) {
76
+ case Step.PROMPT_OVERWRITE:
77
+ return (React.createElement(React.Fragment, null,
78
+ existingConfig?.client && existingConfig?.token && (React.createElement(CredentialsAlreadyExists, { configPath: configPath(), existingConfig: existingConfig })),
79
+ React.createElement(Box, null,
80
+ React.createElement(Text, null,
81
+ "Would you like to overwrite these credentials?",
82
+ " ",
83
+ React.createElement(Text, { dimColor: true }, "(y/N)")),
84
+ React.createElement(ConfirmInput, { isChecked: false, value: shouldOverwriteAnswer, onChange: setShouldOverwriteAnswer, onSubmit: handleSubmitOverwrite }))));
85
+ case Step.PROMPT_NEW_CREDENTIALS:
86
+ return (React.createElement(NoCredentialsOnboarding, { onSaveCredentials: handleSaveCredentials, configPath: configPath() }));
87
+ case Step.DONE_WRITE_SUCCESS:
88
+ return (React.createElement(Box, { flexDirection: "column" },
89
+ React.createElement(Text, { color: "green" }, "Updated credentials"),
90
+ existingConfig?.client && existingConfig?.token && (React.createElement(CredentialsPreview, { client: existingConfig.client, token: existingConfig.token }))));
91
+ case Step.DONE_NO_CHANGE:
92
+ return (React.createElement(Box, { flexDirection: "column" },
93
+ React.createElement(Text, { color: "yellow" }, "Credentials are unchanged"),
94
+ existingConfig?.client && existingConfig?.token && (React.createElement(CredentialsPreview, { client: existingConfig.client, token: existingConfig.token }))));
95
+ case Step.DONE_WRITE_FAIL:
96
+ return React.createElement(Text, { color: "yellow" }, "Something went wrong, try again.");
97
+ case Step.LOADING_CONFIG:
98
+ return (React.createElement(Text, null,
99
+ React.createElement(Text, { color: "green" },
100
+ React.createElement(Spinner, { type: "dots" })),
101
+ " Loading config..."));
102
+ default:
103
+ return React.createElement(Text, null, "Done");
104
+ }
105
+ }
@@ -1,16 +1,13 @@
1
1
  import * as React from "react";
2
2
  interface CredentialsAlreadyExistsProps {
3
- client?: string;
4
- token?: string;
5
3
  configPath: string;
6
4
  existingConfig: {
7
5
  client: string;
8
6
  token: string;
9
7
  };
10
- onOverwriteCredentials: () => Promise<void>;
11
8
  }
12
9
  /**
13
- * Show when credentials already exist and prompt user to overwrite if applicable
10
+ * Show when credentials already exist
14
11
  */
15
- export default function CredentialsAlreadyExists({ client, token, configPath, existingConfig, onOverwriteCredentials, }: CredentialsAlreadyExistsProps): React.JSX.Element;
12
+ export default function CredentialsAlreadyExists({ configPath, existingConfig, }: CredentialsAlreadyExistsProps): React.JSX.Element;
16
13
  export {};
@@ -1,31 +1,16 @@
1
1
  import * as React from "react";
2
- import { Box, Text, useApp } from "ink";
2
+ import { Box, Text } from "ink";
3
3
  import Link from "ink-link";
4
4
  import CredentialsPreview from "./credentials-preview.js";
5
- import ConfirmInput from "../ui/confirm-input.js";
6
5
  /**
7
- * Show when credentials already exist and prompt user to overwrite if applicable
6
+ * Show when credentials already exist
8
7
  */
9
- export default function CredentialsAlreadyExists({ client, token, configPath, existingConfig, onOverwriteCredentials, }) {
10
- const { exit } = useApp();
11
- const [shouldOverwrite, setShouldOverwrite] = React.useState("");
12
- async function handleSubmitOverwrite(override) {
13
- if (override) {
14
- await onOverwriteCredentials();
15
- }
16
- exit();
17
- }
8
+ export default function CredentialsAlreadyExists({ configPath, existingConfig, }) {
18
9
  return (React.createElement(Box, { flexDirection: "column" },
19
10
  React.createElement(Text, { bold: true, color: "yellow" }, "Credentials already exist"),
20
11
  React.createElement(Text, null,
21
12
  "You already have credentials in",
22
13
  " ",
23
14
  React.createElement(Link, { url: configPath }, configPath)),
24
- React.createElement(CredentialsPreview, { client: existingConfig.client, token: existingConfig.token }),
25
- (client || token) && (React.createElement(Box, null,
26
- React.createElement(Text, null,
27
- "Would you like to overwrite these credentials?",
28
- " ",
29
- React.createElement(Text, { dimColor: true }, "(y/N)")),
30
- React.createElement(ConfirmInput, { isChecked: false, value: shouldOverwrite, onChange: setShouldOverwrite, onSubmit: handleSubmitOverwrite })))));
15
+ React.createElement(CredentialsPreview, { client: existingConfig.client, token: existingConfig.token })));
31
16
  }
@@ -34,7 +34,7 @@ export default function NoCredentialsOnboarding({ onSaveCredentials, configPath,
34
34
  field === "write" && (React.createElement(Box, { flexDirection: "column" },
35
35
  React.createElement(CredentialsPreview, { client: client, token: token }),
36
36
  React.createElement(Text, null,
37
- "Would you like to write these credentials to",
37
+ "Write updated credentials to",
38
38
  " ",
39
39
  React.createElement(Link, { url: configPath }, configPath),
40
40
  "?",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroheight/adoption-cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "license": "ISC",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
@@ -49,7 +49,7 @@
49
49
  "@vdemedes/prettier-config": "^2.0.1",
50
50
  "eslint-plugin-react": "^7.32.2",
51
51
  "eslint-plugin-react-hooks": "^4.6.0",
52
- "ink-testing-library": "git+ssh://git@github.com/vadimdemedes/ink-testing-library.git#f44b077e9a05a1d615bab41c72906726d34ea085",
52
+ "ink-testing-library": "^4.0.0",
53
53
  "memfs": "^4.9.2",
54
54
  "mock-stdin": "^1.0.0",
55
55
  "prettier": "^2.8.8",