@dittowords/cli 2.7.1 → 3.0.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.
Files changed (53) hide show
  1. package/README.md +150 -137
  2. package/bin/add-project.js +5 -7
  3. package/bin/add-project.js.map +1 -1
  4. package/bin/api.js +0 -5
  5. package/bin/api.js.map +1 -1
  6. package/bin/config.js +38 -11
  7. package/bin/config.js.map +1 -1
  8. package/bin/ditto.js +66 -57
  9. package/bin/ditto.js.map +1 -1
  10. package/bin/http/fetchVariants.js +26 -0
  11. package/bin/http/fetchVariants.js.map +1 -0
  12. package/bin/init/init.js +17 -6
  13. package/bin/init/init.js.map +1 -1
  14. package/bin/init/project.js +38 -45
  15. package/bin/init/project.js.map +1 -1
  16. package/bin/init/token.js +22 -20
  17. package/bin/init/token.js.map +1 -1
  18. package/bin/pull.js +142 -193
  19. package/bin/pull.js.map +1 -1
  20. package/bin/remove-project.js +2 -7
  21. package/bin/remove-project.js.map +1 -1
  22. package/bin/utils/cleanFileName.js +11 -0
  23. package/bin/utils/cleanFileName.js.map +1 -0
  24. package/bin/utils/generateJsDriver.js +56 -0
  25. package/bin/utils/generateJsDriver.js.map +1 -0
  26. package/bin/utils/getSelectedProjects.js +3 -18
  27. package/bin/utils/getSelectedProjects.js.map +1 -1
  28. package/bin/utils/projectsToText.js +10 -1
  29. package/bin/utils/projectsToText.js.map +1 -1
  30. package/bin/utils/promptForProject.js +2 -3
  31. package/bin/utils/promptForProject.js.map +1 -1
  32. package/bin/utils/quit.js +10 -0
  33. package/bin/utils/quit.js.map +1 -0
  34. package/lib/add-project.ts +6 -9
  35. package/lib/api.ts +0 -5
  36. package/lib/config.ts +57 -19
  37. package/lib/ditto.ts +74 -58
  38. package/lib/http/fetchVariants.ts +30 -0
  39. package/lib/init/init.ts +38 -6
  40. package/lib/init/project.test.ts +3 -3
  41. package/lib/init/project.ts +47 -58
  42. package/lib/init/token.ts +17 -16
  43. package/lib/pull.ts +205 -261
  44. package/lib/remove-project.ts +2 -8
  45. package/lib/types.ts +24 -3
  46. package/lib/utils/cleanFileName.ts +6 -0
  47. package/lib/utils/generateJsDriver.ts +68 -0
  48. package/lib/utils/getSelectedProjects.ts +5 -24
  49. package/lib/utils/projectsToText.ts +11 -1
  50. package/lib/utils/promptForProject.ts +2 -3
  51. package/lib/utils/quit.ts +5 -0
  52. package/package.json +1 -1
  53. package/tsconfig.json +2 -1
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
 
package/lib/pull.ts CHANGED
@@ -9,9 +9,48 @@ import consts from "./consts";
9
9
  import output from "./output";
10
10
  import { collectAndSaveToken } from "./init/token";
11
11
  import sourcesToText from "./utils/sourcesToText";
12
+ import { generateJsDriver } from "./utils/generateJsDriver";
13
+ import { cleanFileName } from "./utils/cleanFileName";
12
14
  import { SourceInformation, Token, Project } from "./types";
15
+ import { fetchVariants } from "./http/fetchVariants";
13
16
 
14
- const NON_DEFAULT_FORMATS = ["flat", "structured", "android", "ios-strings"];
17
+ type SupportedFormat = "flat" | "structured" | "android" | "ios-strings";
18
+
19
+ const SUPPORTED_FORMATS: SupportedFormat[] = [
20
+ "flat",
21
+ "structured",
22
+ "android",
23
+ "ios-strings",
24
+ ];
25
+
26
+ const JSON_FORMATS: SupportedFormat[] = ["flat", "structured"];
27
+
28
+ const FORMAT_EXTENSIONS = {
29
+ flat: ".json",
30
+ structured: ".json",
31
+ android: ".xml",
32
+ "ios-strings": ".strings",
33
+ };
34
+
35
+ const getFormatDataIsValid = {
36
+ flat: (data: string) => data !== "{}",
37
+ structured: (data: string) => data !== "{}",
38
+ android: (data: string) => data.includes("<string"),
39
+ "ios-strings": (data: string) => !!data,
40
+ };
41
+
42
+ const getFormat = (formatFromSource: string | undefined): SupportedFormat => {
43
+ const f = formatFromSource as SupportedFormat | undefined;
44
+ if (f && SUPPORTED_FORMATS.includes(f)) {
45
+ return f;
46
+ }
47
+
48
+ return "flat";
49
+ };
50
+
51
+ const getFormatExtension = (format: SupportedFormat) => {
52
+ return FORMAT_EXTENSIONS[format];
53
+ };
15
54
 
16
55
  const DEFAULT_FORMAT_KEYS = ["projects", "exported_at"];
17
56
  const hasVariantData = (data: any) => {
@@ -31,16 +70,6 @@ async function askForAnotherToken() {
31
70
  await collectAndSaveToken(message);
32
71
  }
33
72
 
34
- function getExtension(format: string) {
35
- if (format === "android") {
36
- return ".xml";
37
- }
38
- if (format === "ios-strings") {
39
- return ".strings";
40
- }
41
- return ".json";
42
- }
43
-
44
73
  /**
45
74
  * For a given variant:
46
75
  * - if format is unspecified, fetch data for all projects from `/projects` and
@@ -51,92 +80,64 @@ function getExtension(format: string) {
51
80
  async function downloadAndSaveVariant(
52
81
  variantApiId: string | null,
53
82
  projects: Project[],
54
- format: string | undefined,
83
+ format: SupportedFormat,
55
84
  status: string | undefined,
85
+ richText: boolean | undefined,
56
86
  token?: Token
57
87
  ) {
58
- const params: Record<string, string | null> = {
59
- variant: variantApiId,
60
- };
61
- if (format) {
62
- params.format = format;
63
- }
64
- if (status) {
65
- params.status = status;
66
- }
67
-
68
- if (format && NON_DEFAULT_FORMATS.includes(format)) {
69
- const savedMessages = await Promise.all(
70
- projects.map(async ({ id, fileName }: Project) => {
71
- const { data } = await api.get(`/projects/${id}`, {
72
- params,
73
- headers: { Authorization: `token ${token}` },
74
- });
75
-
76
- if (!hasVariantData(data)) {
77
- return "";
78
- }
79
-
80
- const extension = getExtension(format);
81
-
82
- const filename =
83
- fileName + ("__" + (variantApiId || "base")) + extension;
84
- const filepath = path.join(consts.TEXT_DIR, filename);
85
-
86
- let dataString = data;
87
- if (extension === ".json") {
88
- dataString = JSON.stringify(data, null, 2);
89
- }
90
-
91
- fs.writeFileSync(filepath, dataString);
92
-
93
- return getSavedMessage(filename);
94
- })
95
- );
88
+ const params: Record<string, string | null> = { variant: variantApiId };
89
+ if (format) params.format = format;
90
+ if (status) params.status = status;
91
+ if (richText) params.includeRichText = richText.toString();
92
+
93
+ const savedMessages = await Promise.all(
94
+ projects.map(async ({ id, fileName }: Project) => {
95
+ const { data } = await api.get(`/projects/${id}`, {
96
+ params,
97
+ headers: { Authorization: `token ${token}` },
98
+ });
99
+
100
+ if (!hasVariantData(data)) {
101
+ return "";
102
+ }
96
103
 
97
- return savedMessages.join("");
98
- } else {
99
- const { data } = await api.get("/projects", {
100
- params: { ...params, projectIds: projects.map(({ id }) => id) },
101
- headers: { Authorization: `token ${token}` },
102
- });
104
+ const extension = getFormatExtension(format);
103
105
 
104
- if (!hasVariantData(data)) {
105
- return "";
106
- }
106
+ const filename = cleanFileName(
107
+ fileName + ("__" + (variantApiId || "base")) + extension
108
+ );
109
+ const filepath = path.join(consts.TEXT_DIR, filename);
107
110
 
108
- const filename = `${variantApiId || "base"}.json`;
109
- const filepath = path.join(consts.TEXT_DIR, filename);
111
+ let dataString = data;
112
+ if (extension === ".json") {
113
+ dataString = JSON.stringify(data, null, 2);
114
+ }
110
115
 
111
- const dataString = JSON.stringify(data, null, 2);
116
+ const dataIsValid = getFormatDataIsValid[format];
117
+ if (!dataIsValid(dataString)) {
118
+ return "";
119
+ }
112
120
 
113
- fs.writeFileSync(filepath, dataString);
121
+ fs.writeFileSync(filepath, dataString);
122
+ return getSavedMessage(filename);
123
+ })
124
+ );
114
125
 
115
- return getSavedMessage(filename);
116
- }
126
+ return savedMessages.join("");
117
127
  }
118
128
 
119
129
  async function downloadAndSaveVariants(
130
+ variants: { apiID: string }[],
120
131
  projects: Project[],
121
- format: string | undefined,
132
+ format: SupportedFormat,
122
133
  status: string | undefined,
123
- token?: Token,
124
- options?: PullOptions
134
+ richText: boolean | undefined,
135
+ token?: Token
125
136
  ) {
126
- const meta = options ? options.meta : {};
127
-
128
- const { data: variants } = await api.get("/variants", {
129
- params: {
130
- ...meta,
131
- projectIds: projects.map(({ id }) => id),
132
- },
133
- headers: { Authorization: `token ${token}` },
134
- });
135
-
136
137
  const messages = await Promise.all([
137
- downloadAndSaveVariant(null, projects, format, status, token),
138
+ downloadAndSaveVariant(null, projects, format, status, richText, token),
138
139
  ...variants.map(({ apiID }: { apiID: string }) =>
139
- downloadAndSaveVariant(apiID, projects, format, status, token)
140
+ downloadAndSaveVariant(apiID, projects, format, status, richText, token)
140
141
  ),
141
142
  ]);
142
143
 
@@ -145,59 +146,44 @@ async function downloadAndSaveVariants(
145
146
 
146
147
  async function downloadAndSaveBase(
147
148
  projects: Project[],
148
- format: string | undefined,
149
+ format: SupportedFormat,
149
150
  status: string | undefined,
151
+ richText?: boolean | undefined,
150
152
  token?: Token,
151
153
  options?: PullOptions
152
154
  ) {
153
- const meta = options ? options.meta : {};
154
-
155
- const params = {
156
- ...meta,
157
- };
158
- if (format) {
159
- params.format = format;
160
- }
161
- if (status) {
162
- params.status = status;
163
- }
164
-
165
- if (format && NON_DEFAULT_FORMATS.includes(format)) {
166
- const savedMessages = await Promise.all(
167
- projects.map(async ({ id, fileName }: Project) => {
168
- const { data } = await api.get(`/projects/${id}`, {
169
- params,
170
- headers: { Authorization: `token ${token}` },
171
- });
172
-
173
- const extension = getExtension(format);
174
- const filename = `${fileName}${extension}`;
175
- const filepath = path.join(consts.TEXT_DIR, filename);
176
-
177
- let dataString = data;
178
- if (extension === ".json") {
179
- dataString = JSON.stringify(data, null, 2);
180
- }
181
-
182
- fs.writeFileSync(filepath, dataString);
183
-
184
- return getSavedMessage(filename);
185
- })
186
- );
187
-
188
- return savedMessages.join("");
189
- } else {
190
- const { data } = await api.get(`/projects`, {
191
- params: { ...params, projectIds: projects.map(({ id }) => id) },
192
- headers: { Authorization: `token ${token}` },
193
- });
155
+ const params = { ...options?.meta };
156
+ if (format) params.format = format;
157
+ if (status) params.status = status;
158
+ if (richText) params.includeRichText = richText.toString();
159
+
160
+ const savedMessages = await Promise.all(
161
+ projects.map(async ({ id, fileName }: Project) => {
162
+ const { data } = await api.get(`/projects/${id}`, {
163
+ params,
164
+ headers: { Authorization: `token ${token}` },
165
+ });
166
+
167
+ const extension = getFormatExtension(format);
168
+ const filename = cleanFileName(`${fileName}__base${extension}`);
169
+ const filepath = path.join(consts.TEXT_DIR, filename);
170
+
171
+ let dataString = data;
172
+ if (extension === ".json") {
173
+ dataString = JSON.stringify(data, null, 2);
174
+ }
194
175
 
195
- const dataString = JSON.stringify(data, null, 2);
176
+ const dataIsValid = getFormatDataIsValid[format];
177
+ if (!dataIsValid(dataString)) {
178
+ return "";
179
+ }
196
180
 
197
- fs.writeFileSync(consts.TEXT_FILE, dataString);
181
+ fs.writeFileSync(filepath, dataString);
182
+ return getSavedMessage(filename);
183
+ })
184
+ );
198
185
 
199
- return getSavedMessage("text.json");
200
- }
186
+ return savedMessages.join("");
201
187
  }
202
188
 
203
189
  function getSavedMessage(file: string) {
@@ -219,166 +205,124 @@ function cleanOutputFiles() {
219
205
  return "Cleaning old output files..\n";
220
206
  }
221
207
 
222
- // compatability with legacy method of specifying project ids
223
- // that is still used by the default format
224
- const stringifyProjectId = (projectId: string) =>
225
- projectId === "ditto_component_library" ? projectId : `project_${projectId}`;
226
-
227
- /**
228
- * Generates an index.js file that can be consumed
229
- * by an SDK - this is a big DX improvement because
230
- * it provides a single entry point to get all data
231
- * (including variants) instead of having to import
232
- * each generated file individually.
233
- *
234
- * The generated file will have a unified format
235
- * independent of the CLI configuration used to fetch
236
- * data from Ditto.
237
- */
238
- function generateJsDriver(
239
- projects: Project[],
240
- variants: boolean,
241
- format: string | undefined
242
- ) {
243
- const fileNames = fs
244
- .readdirSync(consts.TEXT_DIR)
245
- .filter((fileName) => /\.json$/.test(fileName));
246
-
247
- const projectIdsByName: Record<string, string> = projects.reduce(
248
- (agg, project) => {
249
- if (project.fileName) {
250
- return { ...agg, [project.fileName]: project.id };
251
- }
252
- return agg;
253
- },
254
- {}
255
- );
256
-
257
- const data = fileNames.reduce(
258
- (obj: Record<string, Record<string, string>>, fileName) => {
259
- // filename format: {project-name}__{variant-api-id}.json
260
- // file format: flat or structured
261
- if (variants && format) {
262
- const [projectName, rest] = fileName.split("__");
263
- const [variantApiId] = rest.split(".");
264
-
265
- const projectId = projectIdsByName[projectName];
266
- if (!projectId) {
267
- throw new Error(`Couldn't find id for ${projectName}`);
268
- }
269
-
270
- const projectIdStr = stringifyProjectId(projectId);
271
-
272
- if (!obj[projectIdStr]) {
273
- obj[projectIdStr] = {};
274
- }
275
-
276
- obj[projectIdStr][variantApiId] = `require('./${fileName}')`;
277
- }
278
- // filename format: {variant-api-id}.json
279
- // file format: default
280
- else if (variants) {
281
- const file = require(path.resolve(consts.TEXT_DIR, `./${fileName}`));
282
- const [variantApiId] = fileName.split(".");
283
-
284
- Object.keys(file.projects).forEach((projectId) => {
285
- if (!obj[projectId]) {
286
- obj[projectId] = {};
287
- }
288
-
289
- const project = file.projects[projectId];
290
- obj[projectId][variantApiId] = project.frames || project.components;
291
- });
292
- }
293
- // filename format: {project-name}.json
294
- // file format: flat or structured
295
- else if (format) {
296
- const [projectName] = fileName.split(".");
297
- const projectId = projectIdsByName[projectName];
298
- if (!projectId) {
299
- throw new Error(`Couldn't find id for ${projectName}`);
300
- }
301
-
302
- obj[stringifyProjectId(projectId)] = {
303
- base: `require('./${fileName}')`,
304
- };
305
- }
306
- // filename format: text.json (single file)
307
- // file format: default
308
- else {
309
- const file = require(path.resolve(consts.TEXT_DIR, `./${fileName}`));
310
- Object.keys(file.projects).forEach((projectId) => {
311
- const project = file.projects[projectId];
312
- obj[projectId] = { base: project.frames || project.components };
313
- });
314
- }
315
-
316
- return obj;
317
- },
318
- {}
319
- );
320
-
321
- let dataString = `module.exports = ${JSON.stringify(data, null, 2)}`
322
- // remove quotes around require statements
323
- .replace(/"require\((.*)\)"/g, "require($1)");
324
-
325
- const filePath = path.resolve(consts.TEXT_DIR, "index.js");
326
- fs.writeFileSync(filePath, dataString, { encoding: "utf8" });
327
-
328
- return `Generated .js SDK driver at ${output.info(filePath)}`;
329
- }
330
-
331
208
  async function downloadAndSave(
332
- sourceInformation: SourceInformation,
209
+ source: SourceInformation,
333
210
  token?: Token,
334
211
  options?: PullOptions
335
212
  ) {
336
213
  const {
337
214
  validProjects,
338
- variants,
339
- format,
215
+ format: formatFromSource,
340
216
  shouldFetchComponentLibrary,
341
217
  status,
342
- } = sourceInformation;
218
+ richText,
219
+ componentFolders,
220
+ } = source;
343
221
 
344
- let msg = `\nFetching the latest text from ${sourcesToText(
345
- validProjects,
346
- shouldFetchComponentLibrary
347
- )}\n`;
222
+ const format = getFormat(formatFromSource);
348
223
 
224
+ let msg = "";
349
225
  const spinner = ora(msg);
350
226
  spinner.start();
351
227
 
352
- // We'll need to move away from this solution if at some
353
- // point down the road we stop allowing the component
354
- // library to be returned from the /projects endpoint
355
- if (shouldFetchComponentLibrary) {
356
- validProjects.push({
357
- id: "ditto_component_library",
358
- name: "Ditto Component Library",
359
- fileName: "ditto-component-library",
360
- });
361
- }
228
+ const variants = await fetchVariants(source);
362
229
 
363
230
  try {
364
231
  msg += cleanOutputFiles();
232
+ msg += `\nFetching the latest text from ${sourcesToText(
233
+ validProjects,
234
+ shouldFetchComponentLibrary
235
+ )}\n`;
365
236
 
366
237
  const meta = options ? options.meta : {};
367
- msg += variants
368
- ? await downloadAndSaveVariants(validProjects, format, status, token, {
369
- meta,
238
+
239
+ if (shouldFetchComponentLibrary) {
240
+ // Always include a variant with an apiID of undefined to ensure that we
241
+ // fetch the base text for the component library.
242
+ const componentVariants = [{ apiID: undefined }, ...(variants || [])];
243
+
244
+ const params = new URLSearchParams();
245
+ if (options?.meta)
246
+ Object.entries(options.meta).forEach(([k, v]) => params.append(k, v));
247
+ if (format) params.append("format", format);
248
+ if (status) params.append("status", status);
249
+ if (richText) params.append("includeRichText", richText.toString());
250
+ if (componentFolders) {
251
+ componentFolders.forEach(({ id }) => params.append("folder_id[]", id));
252
+ }
253
+
254
+ const messages = await Promise.all(
255
+ componentVariants.map(async ({ apiID: variantApiId }) => {
256
+ const p = new URLSearchParams(params);
257
+ if (variantApiId) p.append("variant", variantApiId);
258
+
259
+ const { data } = await api.get(`/components`, { params: p });
260
+
261
+ const nameExt = getFormatExtension(format);
262
+ const nameBase = "ditto-component-library";
263
+ const namePostfix = `__${variantApiId || "base"}`;
264
+
265
+ const fileName = cleanFileName(`${nameBase}${namePostfix}${nameExt}`);
266
+ const filePath = path.join(consts.TEXT_DIR, fileName);
267
+
268
+ let dataString = data;
269
+ if (nameExt === ".json") {
270
+ dataString = JSON.stringify(data, null, 2);
271
+ }
272
+
273
+ const dataIsValid = getFormatDataIsValid[format];
274
+ if (!dataIsValid(dataString)) {
275
+ return "";
276
+ }
277
+
278
+ await new Promise((r) => fs.writeFile(filePath, dataString, r));
279
+ return getSavedMessage(fileName);
370
280
  })
371
- : await downloadAndSaveBase(validProjects, format, status, token, {
372
- meta,
373
- });
281
+ );
282
+
283
+ msg += messages.join("");
284
+ }
285
+
286
+ if (validProjects.length) {
287
+ msg += variants
288
+ ? await downloadAndSaveVariants(
289
+ variants,
290
+ validProjects,
291
+ format,
292
+ status,
293
+ richText,
294
+ token
295
+ )
296
+ : await downloadAndSaveBase(
297
+ validProjects,
298
+ format,
299
+ status,
300
+ richText,
301
+ token,
302
+ {
303
+ meta,
304
+ }
305
+ );
306
+ }
374
307
 
375
- msg += generateJsDriver(validProjects, variants, format);
308
+ const sources = [...validProjects];
309
+ if (shouldFetchComponentLibrary) {
310
+ sources.push({
311
+ id: "ditto_component_library",
312
+ name: "Ditto Component Library",
313
+ fileName: "ditto-component-library",
314
+ });
315
+ }
316
+
317
+ if (JSON_FORMATS.includes(format)) msg += generateJsDriver(sources);
376
318
 
377
319
  msg += `\n${output.success("Done")}!`;
378
320
 
379
321
  spinner.stop();
380
322
  return console.log(msg);
381
323
  } catch (e: any) {
324
+ console.error(e);
325
+
382
326
  spinner.stop();
383
327
  let error = e.message;
384
328
  if (e.response && e.response.status === 404) {
@@ -415,7 +359,7 @@ async function downloadAndSave(
415
359
  }
416
360
  }
417
361
 
418
- interface PullOptions {
362
+ export interface PullOptions {
419
363
  meta?: Record<string, string>;
420
364
  }
421
365
 
@@ -19,15 +19,9 @@ async function removeProject() {
19
19
  return;
20
20
  }
21
21
 
22
- const allProjects = isUsingComponents
23
- ? [{ id: "components", name: "Ditto Component Library" }, ...projects]
24
- : projects;
25
-
26
22
  const projectToRemove = await promptForProject({
27
- projects: allProjects,
28
- message: isUsingComponents
29
- ? "Select a project or library to remove"
30
- : "Select a project to remove",
23
+ projects,
24
+ message: "Select a project to remove",
31
25
  });
32
26
  if (!projectToRemove) return;
33
27
 
package/lib/types.ts CHANGED
@@ -5,12 +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;
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[];
14
33
  }
15
34
 
16
35
  export interface SourceInformation {
@@ -20,6 +39,8 @@ export interface SourceInformation {
20
39
  variants: boolean;
21
40
  format: string | undefined;
22
41
  status: string | undefined;
42
+ richText: boolean | undefined;
43
+ componentFolders: ComponentFolder[] | null;
23
44
  }
24
45
 
25
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
+ }