@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
package/lib/replace.ts ADDED
@@ -0,0 +1,106 @@
1
+ import fs from "fs-extra";
2
+ import { parse } from "@babel/parser";
3
+ import traverse from "@babel/traverse";
4
+ import * as t from "@babel/types";
5
+ import { transformFromAst } from "@babel/core";
6
+
7
+ async function replaceJSXTextInFile(
8
+ filePath: string,
9
+ replacement: { searchString: string; replaceWith: string }
10
+ ) {
11
+ const code = await fs.readFile(filePath, "utf-8");
12
+ const ast = parse(code, {
13
+ sourceType: "module",
14
+ plugins: ["jsx", "typescript"],
15
+ });
16
+
17
+ traverse(ast, {
18
+ JSXText(path) {
19
+ const { searchString, replaceWith } = replacement;
20
+
21
+ const regex = new RegExp(searchString, "gi");
22
+ if (regex.test(path.node.value)) {
23
+ const splitValues = splitByCaseInsensitive(
24
+ path.node.value,
25
+ searchString
26
+ );
27
+ const nodes: (t.JSXElement | t.JSXText)[] = [];
28
+
29
+ splitValues.forEach((splitValue) => {
30
+ if (splitValue.toLowerCase() === searchString.toLowerCase()) {
31
+ const identifier = t.jsxIdentifier("DittoComponent");
32
+ const componentId = t.jsxAttribute(
33
+ t.jsxIdentifier("componentId"),
34
+ t.stringLiteral(replaceWith)
35
+ );
36
+ const o = t.jsxOpeningElement(identifier, [componentId], true);
37
+ const jsxElement = t.jsxElement(o, undefined, [], true);
38
+ nodes.push(jsxElement);
39
+ } else {
40
+ nodes.push(t.jsxText(splitValue));
41
+ }
42
+ });
43
+
44
+ path.replaceWithMultiple(nodes);
45
+ }
46
+ },
47
+ });
48
+
49
+ // transfromFromAst types are wrong?
50
+ /* @ts-ignore */
51
+ const { code: transformedCode } = transformFromAst(ast, code, {
52
+ // Don't let this codebase's Babel config affect the code we're transforming.
53
+ configFile: false,
54
+ });
55
+ fs.writeFile(filePath, transformedCode);
56
+ }
57
+
58
+ function splitByCaseInsensitive(str: string, delimiter: string) {
59
+ return str.split(new RegExp(`(${delimiter})`, "gi")).filter((s) => s !== "");
60
+ }
61
+
62
+ function replace(options: string[]) {
63
+ let filePath: string;
64
+ let searchString: string;
65
+ let replaceWith: string;
66
+
67
+ try {
68
+ const parsedOptions = parseOptions(options);
69
+ filePath = parsedOptions.filePath;
70
+ searchString = parsedOptions.searchString;
71
+ replaceWith = parsedOptions.replaceWith;
72
+ } catch (e) {
73
+ console.error(e);
74
+ console.error(
75
+ "Usage for replace: ditto-cli replace <file path> <search string> <replace with>"
76
+ );
77
+ return;
78
+ }
79
+
80
+ replaceJSXTextInFile(filePath, { searchString, replaceWith });
81
+ }
82
+
83
+ function parseOptions(options: string[]): {
84
+ filePath: string;
85
+ searchString: string;
86
+ replaceWith: string;
87
+ } {
88
+ if (options.length !== 3) {
89
+ throw new Error(
90
+ "The options array must contain <file path> <search string> <replace with>."
91
+ );
92
+ }
93
+
94
+ const filePath = options[0];
95
+ // Check if the file path exists and points to a regular file (not a directory or other file system object).
96
+ const isFilePathValid =
97
+ fs.existsSync(filePath) && fs.lstatSync(filePath).isFile();
98
+
99
+ if (!isFilePathValid) {
100
+ throw new Error(`${filePath} is not a valid file path.`);
101
+ }
102
+
103
+ return { filePath, searchString: options[1], replaceWith: options[2] };
104
+ }
105
+
106
+ export { replace, parseOptions, replaceJSXTextInFile };
package/lib/types.ts CHANGED
@@ -5,13 +5,31 @@ export interface Project {
5
5
  fileName?: string;
6
6
  }
7
7
 
8
+ export type Source = Project;
9
+
10
+ interface ComponentFolder {
11
+ id: string;
12
+ name: string;
13
+ }
14
+
8
15
  export interface ConfigYAML {
9
- components?: boolean;
10
- projects?: Project[];
11
- format?: string;
16
+ sources?: {
17
+ components?: {
18
+ enabled?: boolean;
19
+ folders?: ComponentFolder[];
20
+ };
21
+ projects?: Project[];
22
+ };
23
+ format?: "flat" | "structured" | "android-xml" | "ios-strings";
12
24
  status?: string;
13
25
  variants?: boolean;
14
26
  richText?: boolean;
27
+
28
+ // these are legacy fields - if they exist, we should output
29
+ // a deprecation error, and suggest that they nest them under
30
+ // a top-level `sources` property
31
+ components?: boolean;
32
+ projects?: Project[];
15
33
  }
16
34
 
17
35
  export interface SourceInformation {
@@ -22,6 +40,7 @@ export interface SourceInformation {
22
40
  format: string | undefined;
23
41
  status: string | undefined;
24
42
  richText: boolean | undefined;
43
+ componentFolders: ComponentFolder[] | null;
25
44
  }
26
45
 
27
46
  export type Token = string | undefined;
@@ -0,0 +1,6 @@
1
+ export function cleanFileName(fileName: string): string {
2
+ return fileName
3
+ .replace(/\s{1,}/g, "-")
4
+ .replace(/[^a-zA-Z0-9-_.]/g, "")
5
+ .toLowerCase();
6
+ }
@@ -0,0 +1,68 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import consts from "../consts";
4
+ import output from "../output";
5
+ import { Source } from "../types";
6
+ import { cleanFileName } from "./cleanFileName";
7
+
8
+ // compatability with legacy method of specifying project ids
9
+ // that is still used by the default format
10
+ const stringifySourceId = (projectId: string) =>
11
+ projectId === "ditto_component_library" ? projectId : `project_${projectId}`;
12
+
13
+ /**
14
+ * Generates an index.js file that can be consumed
15
+ * by an SDK - this is a big DX improvement because
16
+ * it provides a single entry point to get all data
17
+ * (including variants) instead of having to import
18
+ * each generated file individually.
19
+ *
20
+ * The generated file will have a unified format
21
+ * independent of the CLI configuration used to fetch
22
+ * data from Ditto.
23
+ */
24
+
25
+ // TODO: support ESM
26
+ export function generateJsDriver(sources: Source[]) {
27
+ const fileNames = fs
28
+ .readdirSync(consts.TEXT_DIR)
29
+ .filter((fileName) => /\.json$/.test(fileName));
30
+
31
+ const sourceIdsByName: Record<string, string> = sources.reduce(
32
+ (agg, source) => {
33
+ if (source.fileName) {
34
+ return { ...agg, [cleanFileName(source.fileName)]: source.id };
35
+ }
36
+
37
+ return agg;
38
+ },
39
+ {}
40
+ );
41
+
42
+ const data = fileNames.reduce(
43
+ (obj: Record<string, Record<string, string>>, fileName) => {
44
+ const [sourceId, rest] = fileName.split("__");
45
+ const [variantApiId] = rest.split(".");
46
+
47
+ const projectId = sourceIdsByName[sourceId];
48
+ const projectIdStr = stringifySourceId(projectId);
49
+
50
+ if (!obj[projectIdStr]) {
51
+ obj[projectIdStr] = {};
52
+ }
53
+
54
+ obj[projectIdStr][variantApiId] = `require('./${fileName}')`;
55
+ return obj;
56
+ },
57
+ {}
58
+ );
59
+
60
+ let dataString = `module.exports = ${JSON.stringify(data, null, 2)}`
61
+ // remove quotes around require statements
62
+ .replace(/"require\((.*)\)"/g, "require($1)");
63
+
64
+ const filePath = path.resolve(consts.TEXT_DIR, "index.js");
65
+ fs.writeFileSync(filePath, dataString, { encoding: "utf8" });
66
+
67
+ return `Generated .js SDK driver at ${output.info(filePath)}`;
68
+ }
@@ -3,6 +3,7 @@ import yaml, { YAMLException } from "js-yaml";
3
3
 
4
4
  import { PROJECT_CONFIG_FILE } from "../consts";
5
5
  import { ConfigYAML, Project } from "../types";
6
+ import Config, { DEFAULT_CONFIG_JSON } from "../config";
6
7
 
7
8
  function jsonIsConfigYAML(json: unknown): json is ConfigYAML {
8
9
  return typeof json === "object";
@@ -28,28 +29,8 @@ function yamlToJson(_yaml: string): ConfigYAML | null {
28
29
  * Returns an array containing all valid projects ({ id, name })
29
30
  * currently contained in the project config file.
30
31
  */
31
- export const getSelectedProjects = (
32
- configFile = PROJECT_CONFIG_FILE
33
- ): Project[] => {
34
- if (!fs.existsSync(configFile)) return [];
32
+ export const getSelectedProjects = (configFile = PROJECT_CONFIG_FILE) =>
33
+ Config.parseSourceInformation(configFile).validProjects;
35
34
 
36
- const contentYaml = fs.readFileSync(configFile, "utf8");
37
- const contentJson = yamlToJson(contentYaml);
38
-
39
- if (!(contentJson && contentJson.projects)) {
40
- return [];
41
- }
42
-
43
- return contentJson.projects.filter(({ name, id }) => name && id);
44
- };
45
-
46
- export const getIsUsingComponents = (
47
- configFile = PROJECT_CONFIG_FILE
48
- ): boolean => {
49
- if (!fs.existsSync(configFile)) return false;
50
-
51
- const contentYaml = fs.readFileSync(configFile, "utf8");
52
- const contentJson = yamlToJson(contentYaml);
53
-
54
- return !!contentJson && !!contentJson.components;
55
- };
35
+ export const getIsUsingComponents = (configFile = PROJECT_CONFIG_FILE) =>
36
+ Config.parseSourceInformation(configFile).shouldFetchComponentLibrary;
@@ -1,6 +1,16 @@
1
1
  import output from "../output";
2
2
  import { Project } from "../types";
3
3
 
4
+ export const getSourceUrl = (sourceId: string) => {
5
+ let base = "https://app.dittowords.com";
6
+
7
+ if (sourceId === "ditto_component_library") {
8
+ return `${base}/components`;
9
+ }
10
+
11
+ return `${base}/doc/${sourceId}`;
12
+ };
13
+
4
14
  const projectsToText = (projects: Project[]) => {
5
15
  return (
6
16
  (projects || []).reduce(
@@ -10,7 +20,7 @@ const projectsToText = (projects: Project[]) => {
10
20
  "- " +
11
21
  output.info(name) +
12
22
  " " +
13
- output.subtle("https://app.dittowords.com/doc/" + id)),
23
+ output.subtle(getSourceUrl(id))),
14
24
  ""
15
25
  ) + "\n"
16
26
  );
@@ -2,12 +2,11 @@ const { AutoComplete } = require("enquirer");
2
2
 
3
3
  import output from "../output";
4
4
  import { Project } from "../types";
5
+ import { getSourceUrl } from "./projectsToText";
5
6
 
6
7
  function formatProjectChoice(project: Project) {
7
8
  return (
8
- project.name +
9
- " " +
10
- output.subtle(project.url || `https://app.dittowords.com/doc/${project.id}`)
9
+ project.name + " " + output.subtle(project.url || getSourceUrl(project.id))
11
10
  );
12
11
  }
13
12
 
@@ -0,0 +1,5 @@
1
+ export function quit(message: string, exitCode = 2) {
2
+ console.log(`\n${message}\n`);
3
+ process.exitCode = exitCode;
4
+ process.exit();
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dittowords/cli",
3
- "version": "2.8.0",
3
+ "version": "3.2.0-alpha",
4
4
  "description": "Command Line Interface for Ditto (dittowords.com).",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -33,7 +33,6 @@
33
33
  "ditto-cli": "bin/ditto.js"
34
34
  },
35
35
  "devDependencies": {
36
- "@babel/core": "^7.11.4",
37
36
  "@babel/preset-env": "^7.20.2",
38
37
  "@babel/preset-typescript": "^7.18.6",
39
38
  "@tsconfig/node16": "^1.0.3",
@@ -53,12 +52,20 @@
53
52
  "typescript": "^4.7.4"
54
53
  },
55
54
  "dependencies": {
55
+ "@babel/core": "^7.11.4",
56
+ "@babel/parser": "^7.21.4",
57
+ "@babel/traverse": "^7.21.4",
58
+ "@babel/types": "^7.21.4",
59
+ "@types/babel-traverse": "^6.25.7",
60
+ "@types/fs-extra": "^11.0.1",
56
61
  "axios": "^0.27.2",
57
62
  "boxen": "^5.1.2",
58
63
  "chalk": "^4.1.0",
59
64
  "commander": "^6.1.0",
60
65
  "enquirer": "^2.3.6",
61
66
  "faker": "^5.1.0",
67
+ "fs-extra": "^11.1.1",
68
+ "glob": "^9.3.4",
62
69
  "js-yaml": "^4.1.0",
63
70
  "ora": "^5.0.0",
64
71
  "v8-compile-cache": "^2.1.1"
package/tsconfig.json CHANGED
@@ -6,7 +6,8 @@
6
6
  "allowSyntheticDefaultImports": true,
7
7
  "resolveJsonModule": true,
8
8
  "strictNullChecks": true,
9
- "sourceMap": true
9
+ "sourceMap": true,
10
+ "strict": true
10
11
  },
11
12
  "include": ["lib/ditto.ts"]
12
13
  }