@dittowords/cli 1.2.6-beta → 2.1.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/README.md CHANGED
@@ -89,6 +89,8 @@ If you run the CLI in a directory that does not contain a `ditto/` folder, the f
89
89
 
90
90
  An automatically generated driver file that simplifies the process of passing text data to Ditto JavaScript SDKs. This file has a standardized format that is always the same independent of the CLI configuration used to generate it.
91
91
 
92
+ **Since this file is designed to be consumed by other internal Ditto libraries, it is not recommended that you depend on it - its format may change between major releases.**
93
+
92
94
  ```ts
93
95
  interface DriverFile {
94
96
  [projectId: string]: {
package/lib/config.js CHANGED
@@ -1,9 +1,10 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const url = require("url");
4
-
5
4
  const yaml = require("js-yaml");
6
5
 
6
+ const consts = require("./consts");
7
+
7
8
  function createFileIfMissing(filename) {
8
9
  const dir = path.dirname(filename);
9
10
 
@@ -14,7 +15,13 @@ function createFileIfMissing(filename) {
14
15
  }
15
16
  }
16
17
 
17
- function readData(file, defaultData = {}) {
18
+ /**
19
+ * Read data from a file
20
+ * @param {*} file defaults to `PROJECT_CONFIG_FILE` defined in `constants.js`
21
+ * @param {*} defaultData defaults to `{}`
22
+ * @returns
23
+ */
24
+ function readData(file = consts.PROJECT_CONFIG_FILE, defaultData = {}) {
18
25
  createFileIfMissing(file);
19
26
  const fileContents = fs.readFileSync(file, "utf8");
20
27
  return yaml.safeLoad(fileContents) || defaultData;
@@ -72,6 +79,73 @@ function save(file, key, value) {
72
79
  writeData(file, data);
73
80
  }
74
81
 
82
+ const IS_DUPLICATE = /-(\d+$)/;
83
+ function dedupeProjectName(projectNames, projectName) {
84
+ let dedupedName = projectName;
85
+
86
+ if (projectNames[dedupedName]) {
87
+ while (projectNames[dedupedName]) {
88
+ const [_, numberStr] = dedupedName.match(IS_DUPLICATE) || [];
89
+ if (numberStr && !isNaN(parseInt(numberStr))) {
90
+ dedupedName = `${dedupedName.replace(IS_DUPLICATE, "")}-${
91
+ parseInt(numberStr) + 1
92
+ }`;
93
+ } else {
94
+ dedupedName = `${dedupedName}-1`;
95
+ }
96
+ }
97
+ }
98
+
99
+ return dedupedName;
100
+ }
101
+
102
+ /**
103
+ * Reads from the config file, filters out
104
+ * invalid projects, dedupes those remaining, and returns:
105
+ * - whether or not the data required to `pull` is present
106
+ * - whether or not the component library should be fetched
107
+ * - an array of valid, deduped projects
108
+ * - the `variants` and `format` config options
109
+ */
110
+ function parseSourceInformation() {
111
+ const { projects, components, variants, format } = readData();
112
+
113
+ const projectNames = {};
114
+ const validProjects = [];
115
+
116
+ let componentLibraryInProjects = false;
117
+
118
+ (projects || []).forEach((project) => {
119
+ const isValid = project.id && project.name;
120
+ if (!isValid) {
121
+ return;
122
+ }
123
+
124
+ if (project.id === "ditto_component_library") {
125
+ componentLibraryInProjects = true;
126
+ return;
127
+ }
128
+
129
+ project.fileName = dedupeProjectName(projectNames, project.name);
130
+ projectNames[project.fileName] = true;
131
+
132
+ validProjects.push(project);
133
+ });
134
+
135
+ const shouldFetchComponentLibrary =
136
+ !!components || componentLibraryInProjects;
137
+
138
+ const hasSourceData = validProjects.length || shouldFetchComponentLibrary;
139
+
140
+ return {
141
+ hasSourceData,
142
+ validProjects,
143
+ shouldFetchComponentLibrary,
144
+ variants,
145
+ format,
146
+ };
147
+ }
148
+
75
149
  module.exports = {
76
150
  createFileIfMissing,
77
151
  readData,
@@ -81,4 +155,5 @@ module.exports = {
81
155
  deleteToken,
82
156
  getToken,
83
157
  save,
158
+ parseSourceInformation,
84
159
  };
package/lib/init/init.js CHANGED
@@ -2,13 +2,16 @@
2
2
  // expected to be run once per project.
3
3
  const boxen = require("boxen");
4
4
  const chalk = require("chalk");
5
- const getSelectedProjects = require("../utils/getSelectedProjects");
6
5
  const projectsToText = require("../utils/projectsToText");
7
6
 
8
- const { needsProjects, collectAndSaveProject } = require("./project");
7
+ const { needsSource, collectAndSaveProject } = require("./project");
9
8
  const { needsToken, collectAndSaveToken } = require("./token");
10
9
 
11
- const needsInit = () => needsToken() || needsProjects();
10
+ const config = require("../config");
11
+ const output = require("../output");
12
+ const sourcesToText = require("../utils/sourcesToText");
13
+
14
+ const needsInit = () => needsToken() || needsSource();
12
15
 
13
16
  function welcome() {
14
17
  const msg = chalk.white(`${chalk.bold(
@@ -22,19 +25,24 @@ We're glad to have you here.`);
22
25
 
23
26
  async function init() {
24
27
  welcome();
28
+
25
29
  if (needsToken()) {
26
30
  await collectAndSaveToken();
27
31
  }
28
- const selectedProjects = getSelectedProjects();
29
- if (selectedProjects.length) {
30
- console.log(
31
- `You're currently set up to sync text from the following projects: ${projectsToText(
32
- selectedProjects
33
- )}\n`
34
- );
35
- } else {
32
+
33
+ const { hasSourceData, validProjects, shouldFetchComponentLibrary } =
34
+ config.parseSourceInformation();
35
+
36
+ if (!hasSourceData) {
36
37
  await collectAndSaveProject(true);
38
+ return;
37
39
  }
40
+
41
+ const message =
42
+ "You're currently set up to sync text from " +
43
+ sourcesToText(validProjects, shouldFetchComponentLibrary);
44
+
45
+ console.log(message);
38
46
  }
39
47
 
40
48
  module.exports = { needsInit, init };
@@ -20,9 +20,8 @@ function saveProject(file, name, id) {
20
20
  config.writeData(file, { projects });
21
21
  }
22
22
 
23
- function needsProjects(configFile) {
24
- const projects = getSelectedProjects(configFile);
25
- return !(projects && projects.length);
23
+ function needsSource() {
24
+ return !config.parseSourceInformation().hasSourceData;
26
25
  }
27
26
 
28
27
  async function askForAnotherToken() {
@@ -60,7 +59,7 @@ async function collectProject(token, initialize) {
60
59
  const path = process.cwd();
61
60
  if (initialize) {
62
61
  console.log(
63
- `Looks like are no Ditto projects selected for your current directory: ${output.info(
62
+ `Looks like there are no Ditto projects selected for your current directory: ${output.info(
64
63
  path
65
64
  )}.`
66
65
  );
@@ -113,4 +112,4 @@ async function collectAndSaveProject(initialize = false) {
113
112
  }
114
113
  }
115
114
 
116
- module.exports = { needsProjects, collectAndSaveProject };
115
+ module.exports = { needsSource, collectAndSaveProject };
package/lib/pull.js CHANGED
@@ -9,6 +9,7 @@ const consts = require("./consts");
9
9
  const output = require("./output");
10
10
  const { collectAndSaveToken } = require("./init/token");
11
11
  const projectsToText = require("./utils/projectsToText");
12
+ const sourcesToText = require("./utils/sourcesToText");
12
13
 
13
14
  const NON_DEFAULT_FORMATS = ["flat", "structured"];
14
15
 
@@ -30,54 +31,6 @@ async function askForAnotherToken() {
30
31
  await collectAndSaveToken(message);
31
32
  }
32
33
 
33
- function cleanProjectName(projectName) {
34
- return (
35
- projectName
36
- .replace(/\s/g, "-")
37
- // replace double underscore since this is what we use
38
- // to separate project names and API IDs
39
- .replace(/__/g, "_")
40
- .toLowerCase()
41
- .trim()
42
- );
43
- }
44
-
45
- /**
46
- * Return the passed array of projects with `project.name` modified
47
- * to be `${project.name}-${duplicate_number}` for each project
48
- * that has the same name as another project in the original array.
49
- */
50
- const IS_DUPLICATE = /-(\d+$)/;
51
- function getProjectsWithDedupedNames(projects) {
52
- const projectsWithDedupedNames = [];
53
- const projectNames = {};
54
-
55
- projects.forEach(({ id, name }) => {
56
- let dedupedName = name;
57
-
58
- if (projectNames[dedupedName]) {
59
- while (projectNames[dedupedName]) {
60
- const [_, numberStr] = dedupedName.match(IS_DUPLICATE) || [];
61
- if (numberStr && !isNaN(parseInt(numberStr))) {
62
- dedupedName = `${dedupedName.replace(IS_DUPLICATE, "")}-${
63
- parseInt(numberStr) + 1
64
- }`;
65
- } else {
66
- dedupedName = `${dedupedName}-1`;
67
- }
68
- }
69
- }
70
-
71
- projectNames[dedupedName] = true;
72
- projectsWithDedupedNames.push({
73
- id,
74
- name: cleanProjectName(dedupedName),
75
- });
76
- });
77
-
78
- return projectsWithDedupedNames;
79
- }
80
-
81
34
  /**
82
35
  * For a given variant:
83
36
  * - if format is unspecified, fetch data for all projects from `/projects` and
@@ -93,7 +46,7 @@ async function downloadAndSaveVariant(variantApiId, projects, format, token) {
93
46
 
94
47
  if (NON_DEFAULT_FORMATS.includes(format)) {
95
48
  const savedMessages = await Promise.all(
96
- projects.map(async ({ id, name }) => {
49
+ projects.map(async ({ id, fileName }) => {
97
50
  const { data } = await api.get(`/projects/${id}`, {
98
51
  params,
99
52
  headers: { Authorization: `token ${token}` },
@@ -103,7 +56,7 @@ async function downloadAndSaveVariant(variantApiId, projects, format, token) {
103
56
  return "";
104
57
  }
105
58
 
106
- const filename = name + ("__" + (variantApiId || "base")) + ".json";
59
+ const filename = fileName + ("__" + (variantApiId || "base")) + ".json";
107
60
  const filepath = path.join(consts.TEXT_DIR, filename);
108
61
 
109
62
  const dataString = JSON.stringify(data, null, 2);
@@ -162,13 +115,13 @@ async function downloadAndSaveBase(projects, format, token) {
162
115
 
163
116
  if (NON_DEFAULT_FORMATS.includes(format)) {
164
117
  const savedMessages = await Promise.all(
165
- projects.map(async ({ id, name }) => {
118
+ projects.map(async ({ id, fileName }) => {
166
119
  const { data } = await api.get(`/projects/${id}`, {
167
120
  params,
168
121
  headers: { Authorization: `token ${token}` },
169
122
  });
170
123
 
171
- const filename = `${name}.json`;
124
+ const filename = `${fileName}.json`;
172
125
  const filepath = path.join(consts.TEXT_DIR, filename);
173
126
 
174
127
  const dataString = JSON.stringify(data, null, 2);
@@ -195,36 +148,20 @@ async function downloadAndSaveBase(projects, format, token) {
195
148
  }
196
149
 
197
150
  function getSavedMessage(file) {
198
- return `Successfully saved to ${output.info(file)}.\n`;
151
+ return `Successfully saved to ${output.info(file)}\n`;
199
152
  }
200
153
 
201
- async function cleanOutputFiles() {
202
- const exists = await new Promise((resolve) => {
203
- fs.exists(consts.TEXT_DIR, (exists) => resolve(exists));
204
- });
205
-
206
- if (!exists) {
207
- await new Promise((resolve) => fs.mkdir(consts.TEXT_DIR, () => resolve()));
154
+ function cleanOutputFiles() {
155
+ if (!fs.existsSync(consts.TEXT_DIR)) {
156
+ fs.mkdirSync(consts.TEXT_DIR);
208
157
  }
209
158
 
210
- const fileNames = await new Promise((resolve) =>
211
- fs.readdir(consts.TEXT_DIR, (err, fileNames) => {
212
- if (err) throw err;
213
- resolve(fileNames);
214
- })
215
- );
216
-
217
- await Promise.all(
218
- fileNames.map((fileName) => {
219
- if (/\.js(on)?$/.test(fileName)) {
220
- return new Promise((resolve) => {
221
- fs.unlink(path.resolve(consts.TEXT_DIR, fileName), () => resolve());
222
- });
223
- }
224
-
225
- return Promise.resolve();
226
- })
227
- );
159
+ const fileNames = fs.readdirSync(consts.TEXT_DIR);
160
+ fileNames.forEach((fileName) => {
161
+ if (/\.js(on)?$/.test(fileName)) {
162
+ fs.unlinkSync(path.resolve(consts.TEXT_DIR, fileName));
163
+ }
164
+ });
228
165
 
229
166
  return "Cleaning old output files..\n";
230
167
  }
@@ -251,7 +188,7 @@ function generateJsDriver(projects, variants, format) {
251
188
  .filter((fileName) => /\.json$/.test(fileName));
252
189
 
253
190
  const projectIdsByName = projects.reduce(
254
- (agg, project) => ({ ...agg, [project.name]: project.id }),
191
+ (agg, project) => ({ ...agg, [project.fileName]: project.id }),
255
192
  {}
256
193
  );
257
194
 
@@ -321,28 +258,40 @@ function generateJsDriver(projects, variants, format) {
321
258
  const filePath = path.resolve(consts.TEXT_DIR, "index.js");
322
259
  fs.writeFileSync(filePath, dataString, { encoding: "utf8" });
323
260
 
324
- return `Generated .js SDK driver at ${output.info(filePath)}..`;
261
+ return `Generated .js SDK driver at ${output.info(filePath)}`;
325
262
  }
326
263
 
327
- async function downloadAndSave(projectConfig, token) {
328
- const { projects, variants, format } = projectConfig;
329
- const projectsDeduped = getProjectsWithDedupedNames(projects);
264
+ async function downloadAndSave(sourceInformation, token) {
265
+ const { validProjects, variants, format, shouldFetchComponentLibrary } =
266
+ sourceInformation;
330
267
 
331
- let msg = `\nFetching the latest text from your selected projects: ${projectsToText(
332
- projects
268
+ let msg = `\nFetching the latest text from ${sourcesToText(
269
+ validProjects,
270
+ shouldFetchComponentLibrary
333
271
  )}\n`;
334
272
 
335
273
  const spinner = ora(msg);
336
274
  spinner.start();
337
275
 
276
+ // We'll need to move away from this solution if at some
277
+ // point down the road we stop allowing the component
278
+ // library to be returned from the /projects endpoint
279
+ if (shouldFetchComponentLibrary) {
280
+ validProjects.push({
281
+ id: "ditto_component_library",
282
+ name: "Ditto Component Library",
283
+ fileName: "ditto-component-library",
284
+ });
285
+ }
286
+
338
287
  try {
339
- msg += await cleanOutputFiles();
288
+ msg += cleanOutputFiles();
340
289
 
341
290
  msg += variants
342
- ? await downloadAndSaveVariants(projectsDeduped, format, token)
343
- : await downloadAndSaveBase(projectsDeduped, format, token);
291
+ ? await downloadAndSaveVariants(validProjects, format, token)
292
+ : await downloadAndSaveBase(validProjects, format, token);
344
293
 
345
- msg += generateJsDriver(projectsDeduped, variants, format);
294
+ msg += generateJsDriver(validProjects, variants, format);
346
295
 
347
296
  msg += `\n${output.success("Done")}!`;
348
297
 
@@ -387,15 +336,15 @@ async function downloadAndSave(projectConfig, token) {
387
336
 
388
337
  function pull() {
389
338
  const token = config.getToken(consts.CONFIG_FILE, consts.API_HOST);
390
- const pConfig = config.readData(consts.PROJECT_CONFIG_FILE);
391
- return downloadAndSave(pConfig, token);
339
+ const sourceInformation = config.parseSourceInformation();
340
+
341
+ return downloadAndSave(sourceInformation, token);
392
342
  }
393
343
 
394
344
  module.exports = {
395
345
  pull,
396
346
  _testing: {
397
347
  cleanOutputFiles,
398
- getProjectsWithDedupedNames,
399
348
  downloadAndSaveVariant,
400
349
  downloadAndSaveVariants,
401
350
  downloadAndSaveBase,
@@ -29,7 +29,7 @@ function getSelectedProjects(configFile = PROJECT_CONFIG_FILE) {
29
29
  return [];
30
30
  }
31
31
 
32
- return contentJson.projects.filter(({ name, id }) => name && id);
32
+ return contentjson.projects.filter(({ name, id }) => name && id);
33
33
  }
34
34
 
35
35
  module.exports = getSelectedProjects;
@@ -2,7 +2,7 @@ const output = require("../output");
2
2
 
3
3
  function projectsToText(projects) {
4
4
  return (
5
- projects.reduce(
5
+ (projects || []).reduce(
6
6
  (outputString, { name, id }) =>
7
7
  outputString +
8
8
  ("\n" +
@@ -0,0 +1,24 @@
1
+ const output = require("../output");
2
+ const projectsToText = require("./projectsToText");
3
+
4
+ function sourcesToText(projects, componentLibrary) {
5
+ let message = "";
6
+
7
+ if (componentLibrary) {
8
+ message += `the ${output.info("Ditto Component Library")}`;
9
+
10
+ if ((projects || []).length) {
11
+ message += " and ";
12
+ } else {
13
+ message += "..";
14
+ }
15
+ }
16
+
17
+ if ((projects || []).length) {
18
+ message += ` the following projects: ${projectsToText(projects)}\n`;
19
+ }
20
+
21
+ return message;
22
+ }
23
+
24
+ module.exports = sourcesToText;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dittowords/cli",
3
- "version": "1.2.6-beta",
3
+ "version": "2.1.0",
4
4
  "description": "Command Line Interface for Ditto (dittowords.com).",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {