@dittowords/cli 2.8.0 → 3.2.0-alpha

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.
Files changed (61) hide show
  1. package/README.md +150 -141
  2. package/bin/add-project.js +5 -7
  3. package/bin/add-project.js.map +1 -1
  4. package/bin/config.js +37 -11
  5. package/bin/config.js.map +1 -1
  6. package/bin/ditto.js +82 -57
  7. package/bin/ditto.js.map +1 -1
  8. package/bin/generate-suggestions.js +71 -0
  9. package/bin/generate-suggestions.js.map +1 -0
  10. package/bin/http/fetchComponents.js +13 -0
  11. package/bin/http/fetchComponents.js.map +1 -0
  12. package/bin/http/fetchVariants.js +26 -0
  13. package/bin/http/fetchVariants.js.map +1 -0
  14. package/bin/init/init.js +17 -6
  15. package/bin/init/init.js.map +1 -1
  16. package/bin/init/project.js +38 -45
  17. package/bin/init/project.js.map +1 -1
  18. package/bin/init/token.js +22 -20
  19. package/bin/init/token.js.map +1 -1
  20. package/bin/pull.js +136 -193
  21. package/bin/pull.js.map +1 -1
  22. package/bin/remove-project.js +2 -7
  23. package/bin/remove-project.js.map +1 -1
  24. package/bin/replace.js +107 -0
  25. package/bin/replace.js.map +1 -0
  26. package/bin/utils/cleanFileName.js +11 -0
  27. package/bin/utils/cleanFileName.js.map +1 -0
  28. package/bin/utils/generateJsDriver.js +56 -0
  29. package/bin/utils/generateJsDriver.js.map +1 -0
  30. package/bin/utils/getSelectedProjects.js +3 -18
  31. package/bin/utils/getSelectedProjects.js.map +1 -1
  32. package/bin/utils/projectsToText.js +10 -1
  33. package/bin/utils/projectsToText.js.map +1 -1
  34. package/bin/utils/promptForProject.js +2 -3
  35. package/bin/utils/promptForProject.js.map +1 -1
  36. package/bin/utils/quit.js +10 -0
  37. package/bin/utils/quit.js.map +1 -0
  38. package/lib/add-project.ts +6 -9
  39. package/lib/config.ts +56 -19
  40. package/lib/ditto.ts +111 -60
  41. package/lib/generate-suggestions.test.ts +65 -0
  42. package/lib/generate-suggestions.ts +107 -0
  43. package/lib/http/fetchComponents.ts +14 -0
  44. package/lib/http/fetchVariants.ts +30 -0
  45. package/lib/init/init.ts +38 -6
  46. package/lib/init/project.test.ts +3 -3
  47. package/lib/init/project.ts +47 -58
  48. package/lib/init/token.ts +17 -16
  49. package/lib/pull.ts +199 -279
  50. package/lib/remove-project.ts +2 -8
  51. package/lib/replace.test.ts +101 -0
  52. package/lib/replace.ts +106 -0
  53. package/lib/types.ts +22 -3
  54. package/lib/utils/cleanFileName.ts +6 -0
  55. package/lib/utils/generateJsDriver.ts +68 -0
  56. package/lib/utils/getSelectedProjects.ts +5 -24
  57. package/lib/utils/projectsToText.ts +11 -1
  58. package/lib/utils/promptForProject.ts +2 -3
  59. package/lib/utils/quit.ts +5 -0
  60. package/package.json +9 -2
  61. package/tsconfig.json +2 -1
@@ -0,0 +1,107 @@
1
+ import fs from "fs-extra";
2
+ import glob from "glob";
3
+ import { parse } from "@babel/parser";
4
+ import traverse from "@babel/traverse";
5
+
6
+ import { fetchComponents } from "./http/fetchComponents";
7
+
8
+ async function generateSuggestions() {
9
+ const components = await fetchComponents();
10
+ const results: {
11
+ [compApiId: string]: FindResults;
12
+ } = {};
13
+
14
+ for (const [compApiId, component] of Object.entries(components)) {
15
+ if (!results[compApiId]) {
16
+ results[compApiId] = [];
17
+ }
18
+ const result = await findTextInJSXFiles(".", component.text);
19
+ results[compApiId] = [...results[compApiId], ...result];
20
+ }
21
+
22
+ // Display results to user
23
+ console.log(JSON.stringify(results, null, 2));
24
+ }
25
+
26
+ interface Occurence {
27
+ lineNumber: number;
28
+ preview: string;
29
+ }
30
+
31
+ type FindResults = { file: string; occurrences: Occurence[] }[];
32
+
33
+ async function findTextInJSXFiles(
34
+ path: string,
35
+ searchString: string
36
+ ): Promise<FindResults> {
37
+ const result: { file: string; occurrences: Occurence[] }[] = [];
38
+ const files = glob.sync(`${path}/**/*.+(jsx|tsx)`, {
39
+ ignore: "**/node_modules/**",
40
+ });
41
+
42
+ const promises: Promise<any>[] = [];
43
+
44
+ for (const file of files) {
45
+ promises.push(
46
+ fs.readFile(file, "utf-8").then((code) => {
47
+ const ast = parse(code, {
48
+ sourceType: "module",
49
+ plugins: ["jsx", "typescript"],
50
+ });
51
+
52
+ const occurrences: Occurence[] = [];
53
+
54
+ traverse(ast, {
55
+ JSXText(path) {
56
+ if (path.node.value.includes(searchString)) {
57
+ const regex = new RegExp(searchString, "g");
58
+ let match;
59
+ while ((match = regex.exec(path.node.value)) !== null) {
60
+ const lines = path.node.value.slice(0, match.index).split("\n");
61
+
62
+ if (!path.node.loc) {
63
+ continue;
64
+ }
65
+
66
+ const lineNumber = path.node.loc.start.line + lines.length - 1;
67
+
68
+ const codeLines = code.split("\n");
69
+ const line = codeLines[lineNumber - 1];
70
+ const preview = replaceAt(
71
+ line,
72
+ match.index,
73
+ searchString,
74
+ `{{${searchString}}}`
75
+ );
76
+
77
+ occurrences.push({ lineNumber, preview });
78
+ }
79
+ }
80
+ },
81
+ });
82
+
83
+ if (occurrences.length > 0) {
84
+ result.push({ file, occurrences });
85
+ }
86
+ })
87
+ );
88
+ }
89
+
90
+ await Promise.all(promises);
91
+
92
+ return result;
93
+ }
94
+
95
+ function replaceAt(
96
+ str: string,
97
+ index: number,
98
+ searchString: string,
99
+ replacement: string
100
+ ) {
101
+ return (
102
+ str.substring(0, index) +
103
+ str.substring(index, str.length).replace(searchString, replacement)
104
+ );
105
+ }
106
+
107
+ export { findTextInJSXFiles, generateSuggestions };
@@ -0,0 +1,14 @@
1
+ import api from "../api";
2
+
3
+ export async function fetchComponents() {
4
+ const { data } = await api.get<{
5
+ [compApiId: string]: {
6
+ name: string;
7
+ text: string;
8
+ status: "NONE" | "WIP" | "REVIEW" | "FINAL";
9
+ folder: "string" | null;
10
+ };
11
+ }>("/components", {});
12
+
13
+ return data;
14
+ }
@@ -0,0 +1,30 @@
1
+ import { AxiosRequestConfig } from "axios";
2
+ import api from "../api";
3
+ import { PullOptions } from "../pull";
4
+ import { SourceInformation } from "../types";
5
+
6
+ export async function fetchVariants(
7
+ source: SourceInformation,
8
+ options: PullOptions = {}
9
+ ) {
10
+ if (!source.variants) {
11
+ return null;
12
+ }
13
+
14
+ const { shouldFetchComponentLibrary, validProjects } = source;
15
+
16
+ const config: AxiosRequestConfig = {
17
+ params: { ...options?.meta },
18
+ };
19
+
20
+ // if we're not syncing from the component library, then we pass the project ids
21
+ // to limit the list of returned variants to only those that are relevant for the
22
+ // specified projects
23
+ if (validProjects.length && !shouldFetchComponentLibrary) {
24
+ config.params.projectIds = validProjects.map(({ id }) => id);
25
+ }
26
+
27
+ const { data } = await api.get<{ apiID: string }[]>("/variants", config);
28
+
29
+ return data;
30
+ }
package/lib/init/init.ts CHANGED
@@ -4,13 +4,15 @@ import boxen from "boxen";
4
4
  import chalk from "chalk";
5
5
  import projectsToText from "../utils/projectsToText";
6
6
 
7
- import { needsSource, collectAndSaveProject } from "./project";
7
+ import { needsSource, collectAndSaveSource } from "./project";
8
8
  import { needsToken, collectAndSaveToken } from "./token";
9
9
 
10
10
  import config from "../config";
11
+ import output from "../output";
11
12
  import sourcesToText from "../utils/sourcesToText";
13
+ import { quit } from "../utils/quit";
12
14
 
13
- export const needsInit = () => needsToken() || needsSource();
15
+ export const needsTokenOrSource = () => needsToken() || needsSource();
14
16
 
15
17
  function welcome() {
16
18
  const msg = chalk.white(`${chalk.bold(
@@ -29,11 +31,41 @@ export const init = async () => {
29
31
  await collectAndSaveToken();
30
32
  }
31
33
 
32
- const { hasSourceData, validProjects, shouldFetchComponentLibrary } =
33
- config.parseSourceInformation();
34
+ const {
35
+ hasSourceData,
36
+ validProjects,
37
+ shouldFetchComponentLibrary,
38
+ hasTopLevelComponentsField,
39
+ hasTopLevelProjectsField,
40
+ } = config.parseSourceInformation();
41
+
42
+ if (hasTopLevelProjectsField) {
43
+ return quit(`${output.errorText(
44
+ `Support for ${output.warnText(
45
+ "projects"
46
+ )} as a top-level field has been removed; please configure ${output.warnText(
47
+ "sources.projects"
48
+ )} instead.`
49
+ )}
50
+ See ${output.url("https://github.com/dittowords/cli")} for more information.`);
51
+ }
52
+
53
+ if (hasTopLevelComponentsField) {
54
+ return quit(
55
+ `${output.errorText(
56
+ "Support for `components` as a top-level field has been removed; please configure `sources.components` instead."
57
+ )}
58
+ See ${output.url("https://github.com/dittowords/cli")} for more information.`
59
+ );
60
+ }
34
61
 
35
62
  if (!hasSourceData) {
36
- await collectAndSaveProject(true);
63
+ console.log(
64
+ `Looks like there are no Ditto sources selected for your current directory: ${output.info(
65
+ process.cwd()
66
+ )}.`
67
+ );
68
+ await collectAndSaveSource({ initialize: true, components: true });
37
69
  return;
38
70
  }
39
71
 
@@ -44,4 +76,4 @@ export const init = async () => {
44
76
  console.log(message);
45
77
  };
46
78
 
47
- export default { needsInit, init };
79
+ export default { init };
@@ -42,8 +42,8 @@ describe("saveProject", () => {
42
42
  it("creates a config file with config data", () => {
43
43
  const fileContents = fs.readFileSync(configFile, "utf8");
44
44
  const data = yaml.load(fileContents);
45
- expect(data.projects).toBeDefined();
46
- expect(data.projects[0].name).toEqual(projectName);
47
- expect(data.projects[0].id).toEqual(projectId);
45
+ expect(data.sources.projects).toBeDefined();
46
+ expect(data.sources.projects[0].name).toEqual(projectName);
47
+ expect(data.sources.projects[0].id).toEqual(projectId);
48
48
  });
49
49
  });
@@ -12,25 +12,18 @@ import {
12
12
  import promptForProject from "../utils/promptForProject";
13
13
  import { AxiosResponse } from "axios";
14
14
  import { Project, Token } from "../types";
15
-
16
- function quit(exitCode = 2) {
17
- console.log("\nExiting Ditto CLI...\n");
18
- process.exitCode = exitCode;
19
- process.exit();
20
- }
15
+ import { quit } from "../utils/quit";
21
16
 
22
17
  function saveProject(file: string, name: string, id: string) {
23
- // old functionality included "ditto_component_library" in the `projects`
24
- // array, but we want to always treat the component library as a separate
25
- // entity and use the new notation of a top-level `components` key
26
18
  if (id === "components") {
27
- config.writeProjectConfigData(file, { components: true });
19
+ config.writeProjectConfigData(file, {
20
+ sources: { components: { enabled: true } },
21
+ });
28
22
  return;
29
23
  }
30
24
 
31
- const projects = [...getSelectedProjects(), { name, id }];
32
-
33
- config.writeProjectConfigData(file, { projects });
25
+ const projects = [...getSelectedProjects(file), { name, id }];
26
+ config.writeProjectConfigData(file, { sources: { projects } });
34
27
  }
35
28
 
36
29
  export const needsSource = () => {
@@ -44,12 +37,8 @@ async function askForAnotherToken() {
44
37
  await collectAndSaveToken(message);
45
38
  }
46
39
 
47
- async function listProjects(
48
- token: Token,
49
- projectsAlreadySelected: Project[],
50
- componentsSelected: boolean
51
- ) {
52
- const spinner = ora("Fetching projects in your workspace...");
40
+ async function listProjects(token: Token, projectsAlreadySelected: Project[]) {
41
+ const spinner = ora("Fetching sources in your workspace...");
53
42
  spinner.start();
54
43
 
55
44
  let response: AxiosResponse<{ id: string; name: string }[]>;
@@ -64,35 +53,37 @@ async function listProjects(
64
53
  throw e;
65
54
  }
66
55
 
56
+ const projectsAlreadySelectedSet = projectsAlreadySelected.reduce(
57
+ (set, project) => set.add(project.id.toString()),
58
+ new Set<string>()
59
+ );
60
+
61
+ const result = response.data.filter(
62
+ ({ id }) =>
63
+ // covers an edge case where v0 of the API includes the component library
64
+ // in the response from the `/project-names` endpoint
65
+ id !== "ditto_component_library" &&
66
+ !projectsAlreadySelectedSet.has(id.toString())
67
+ );
68
+
67
69
  spinner.stop();
68
- return response.data.filter(({ id }: Project) => {
69
- if (id === "ditto_component_library") {
70
- return !componentsSelected;
71
- } else {
72
- return !projectsAlreadySelected.some((project) => project.id === id);
73
- }
74
- });
75
- }
76
70
 
77
- async function collectProject(token: Token, initialize: boolean) {
78
- const path = process.cwd();
79
- if (initialize) {
80
- console.log(
81
- `Looks like there are no Ditto projects selected for your current directory: ${output.info(
82
- path
83
- )}.`
84
- );
85
- }
71
+ return result;
72
+ }
86
73
 
74
+ async function collectSource(token: Token, includeComponents: boolean) {
87
75
  const projectsAlreadySelected = getSelectedProjects();
88
- const usingComponents = getIsUsingComponents();
89
- const projects = await listProjects(
90
- token,
91
- projectsAlreadySelected,
92
- usingComponents
93
- );
76
+ const componentSourceSelected = getIsUsingComponents();
77
+
78
+ let sources = await listProjects(token, projectsAlreadySelected);
79
+ if (includeComponents && !componentSourceSelected) {
80
+ sources = [
81
+ { id: "ditto_component_library", name: "Ditto Component Library" },
82
+ ...sources,
83
+ ];
84
+ }
94
85
 
95
- if (!(projects && projects.length)) {
86
+ if (!sources?.length) {
96
87
  console.log("You're currently syncing all projects in your workspace.");
97
88
  console.log(
98
89
  output.warnText(
@@ -102,24 +93,22 @@ async function collectProject(token: Token, initialize: boolean) {
102
93
  return null;
103
94
  }
104
95
 
105
- const nonInitPrompt = usingComponents
106
- ? "Add a project"
107
- : "Add a project or library";
108
-
109
96
  return promptForProject({
110
- projects,
111
- message: initialize
112
- ? "Choose the project or library you'd like to sync text from"
113
- : nonInitPrompt,
97
+ projects: sources,
98
+ message: "Choose the source you'd like to sync text from",
114
99
  });
115
100
  }
116
101
 
117
- export const collectAndSaveProject = async (initialize = false) => {
102
+ export const collectAndSaveSource = async (
103
+ { components = false }: { initialize?: boolean; components?: boolean } = {
104
+ components: false,
105
+ }
106
+ ) => {
118
107
  try {
119
108
  const token = config.getToken(consts.CONFIG_FILE, consts.API_HOST);
120
- const project = await collectProject(token, initialize);
109
+ const project = await collectSource(token, components);
121
110
  if (!project) {
122
- quit(0);
111
+ quit("", 0);
123
112
  return;
124
113
  }
125
114
 
@@ -127,7 +116,7 @@ export const collectAndSaveProject = async (initialize = false) => {
127
116
  "\n" +
128
117
  `Thanks for adding ${output.info(
129
118
  project.name
130
- )} to your selected projects.\n` +
119
+ )} to your selected sources.\n` +
131
120
  `We saved your updated configuration to: ${output.info(
132
121
  consts.PROJECT_CONFIG_FILE
133
122
  )}\n`
@@ -138,13 +127,13 @@ export const collectAndSaveProject = async (initialize = false) => {
138
127
  console.log(e);
139
128
  if (e.response && e.response.status === 404) {
140
129
  await askForAnotherToken();
141
- await collectAndSaveProject();
130
+ await collectAndSaveSource({ components });
142
131
  } else {
143
- quit();
132
+ quit("", 2);
144
133
  }
145
134
  }
146
135
  };
147
136
 
148
137
  export const _testing = { saveProject, needsSource };
149
138
 
150
- export default { needsSource, collectAndSaveProject };
139
+ export default { needsSource, collectAndSaveSource };
package/lib/init/token.ts CHANGED
@@ -8,6 +8,7 @@ import { create } from "../api";
8
8
  import consts from "../consts";
9
9
  import output from "../output";
10
10
  import config from "../config";
11
+ import { quit } from "../utils/quit";
11
12
 
12
13
  export const needsToken = (configFile?: string, host = consts.API_HOST) => {
13
14
  if (config.getTokenFromEnv()) {
@@ -30,9 +31,9 @@ async function checkToken(token: string): Promise<any> {
30
31
  const axios = create(token);
31
32
  const endpoint = "/token-check";
32
33
 
33
- const resOrError = await axios
34
- .get(endpoint)
35
- .catch((error: any) => {
34
+ let resOrError;
35
+ try {
36
+ resOrError = await axios.get(endpoint).catch((error: any) => {
36
37
  if (error.code === "ENOTFOUND") {
37
38
  return output.errorText(
38
39
  `Can't connect to API: ${output.url(error.hostname)}`
@@ -44,13 +45,19 @@ async function checkToken(token: string): Promise<any> {
44
45
  );
45
46
  }
46
47
  return output.warnText("We're having trouble reaching the Ditto API.");
47
- })
48
- .catch(() =>
49
- output.errorText("Sorry! We're having trouble reaching the Ditto API.")
50
- );
51
- if (typeof resOrError === "string") return resOrError;
48
+ });
49
+ } catch (e: unknown) {
50
+ output.errorText(e as any);
51
+ output.errorText("Sorry! We're having trouble reaching the Ditto API.");
52
+ }
52
53
 
53
- if (resOrError.status === 200) return true;
54
+ if (typeof resOrError === "string") {
55
+ return resOrError;
56
+ }
57
+
58
+ if (resOrError?.status === 200) {
59
+ return true;
60
+ }
54
61
 
55
62
  return output.errorText("This API key isn't valid. Please try another.");
56
63
  }
@@ -75,12 +82,6 @@ async function collectToken(message: string | null) {
75
82
  return response.token;
76
83
  }
77
84
 
78
- function quit(exitCode = 2) {
79
- console.log("API key was not saved.");
80
- process.exitCode = exitCode;
81
- process.exit();
82
- }
83
-
84
85
  /**
85
86
  *
86
87
  * @param {string | null} message
@@ -99,7 +100,7 @@ export const collectAndSaveToken = async (message: string | null = null) => {
99
100
  config.saveToken(consts.CONFIG_FILE, consts.API_HOST, token);
100
101
  return token;
101
102
  } catch (error) {
102
- quit();
103
+ quit("API token was not saved");
103
104
  }
104
105
  };
105
106