@dittowords/cli 4.0.0-alpha.0 → 4.0.1-alpha.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/lib/config.ts CHANGED
@@ -5,12 +5,12 @@ import yaml from "js-yaml";
5
5
  import * as Sentry from "@sentry/node";
6
6
 
7
7
  import consts from "./consts";
8
- import { Project, ConfigYAML } from "./types";
9
8
  import { createSentryContext } from "./utils/createSentryContext";
9
+ import { Project, ConfigYAML, SourceInformation } from "./types";
10
10
 
11
11
  export const DEFAULT_CONFIG_JSON: ConfigYAML = {
12
12
  sources: {
13
- components: { enabled: true },
13
+ components: true,
14
14
  },
15
15
  variants: true,
16
16
  format: "flat",
@@ -188,7 +188,7 @@ function dedupeProjectName(projectNames: Set<string>, projectName: string) {
188
188
  * - an array of valid, deduped projects
189
189
  * - the `variants` and `format` config options
190
190
  */
191
- function parseSourceInformation(file?: string) {
191
+ function parseSourceInformation(file?: string): SourceInformation {
192
192
  const {
193
193
  sources,
194
194
  variants,
@@ -222,8 +222,20 @@ function parseSourceInformation(file?: string) {
222
222
  validProjects.push(project);
223
223
  });
224
224
 
225
- const shouldFetchComponentLibrary = Boolean(sources?.components?.enabled);
226
-
225
+ const shouldFetchComponentLibrary = Boolean(sources?.components);
226
+ const componentRoot =
227
+ typeof sources?.components === "object"
228
+ ? sources.components.root
229
+ : undefined;
230
+ const componentFolders =
231
+ typeof sources?.components === "object"
232
+ ? sources.components.folders
233
+ : undefined;
234
+
235
+ /**
236
+ * If it's not specified to fetch projects or the component library, then there
237
+ * is no source data to pull.
238
+ */
227
239
  const hasSourceData = !!validProjects.length || shouldFetchComponentLibrary;
228
240
 
229
241
  const result = {
@@ -237,7 +249,8 @@ function parseSourceInformation(file?: string) {
237
249
  hasTopLevelProjectsField: !!projectsRoot,
238
250
  hasTopLevelComponentsField: !!componentsRoot,
239
251
  hasComponentLibraryInProjects,
240
- componentFolders: sources?.components?.folders || null,
252
+ componentRoot,
253
+ componentFolders,
241
254
  };
242
255
 
243
256
  Sentry.setContext("config", createSentryContext(result));
@@ -17,7 +17,7 @@ import { quit } from "../utils/quit";
17
17
  function saveProject(file: string, name: string, id: string) {
18
18
  if (id === "components") {
19
19
  config.writeProjectConfigData(file, {
20
- sources: { components: { enabled: true } },
20
+ sources: { components: true },
21
21
  });
22
22
  return;
23
23
  }
package/lib/pull.ts CHANGED
@@ -12,11 +12,25 @@ import { collectAndSaveToken } from "./init/token";
12
12
  import sourcesToText from "./utils/sourcesToText";
13
13
  import { generateJsDriver } from "./utils/generateJsDriver";
14
14
  import { cleanFileName } from "./utils/cleanFileName";
15
- import { SourceInformation, Token, Project, SupportedFormat } from "./types";
15
+ import {
16
+ SourceInformation,
17
+ Token,
18
+ Project,
19
+ SupportedFormat,
20
+ ComponentFolder,
21
+ ComponentSource,
22
+ Source,
23
+ } from "./types";
16
24
  import { fetchVariants } from "./http/fetchVariants";
17
- import { kMaxLength } from "buffer";
18
25
  import { quit } from "./utils/quit";
19
26
  import { AxiosError } from "axios";
27
+ import { fetchComponentFolders } from "./http/fetchComponentFolders";
28
+
29
+ const ensureEndsWithNewLine = (str: string) =>
30
+ str + (/[\r\n]$/.test(str) ? "" : "\n");
31
+
32
+ const writeFile = (path: string, data: string) =>
33
+ new Promise((r) => fs.writeFile(path, ensureEndsWithNewLine(data), r));
20
34
 
21
35
  const SUPPORTED_FORMATS: SupportedFormat[] = [
22
36
  "flat",
@@ -41,7 +55,7 @@ const FORMAT_EXTENSIONS = {
41
55
  const getJsonFormatIsValid = (data: string) => {
42
56
  try {
43
57
  return Object.keys(JSON.parse(data)).some(
44
- (k) => !k.startsWith("__variant")
58
+ (k) => !k.startsWith("__variant"),
45
59
  );
46
60
  } catch {
47
61
  return false;
@@ -59,12 +73,12 @@ export const getFormatDataIsValid = {
59
73
  };
60
74
 
61
75
  const getFormat = (
62
- formatFromSource: string | string[] | undefined
76
+ formatFromSource: string | string[] | undefined,
63
77
  ): SupportedFormat[] => {
64
78
  const formats = (
65
79
  Array.isArray(formatFromSource) ? formatFromSource : [formatFromSource]
66
80
  ).filter((format) =>
67
- SUPPORTED_FORMATS.includes(format as SupportedFormat)
81
+ SUPPORTED_FORMATS.includes(format as SupportedFormat),
68
82
  ) as SupportedFormat[];
69
83
 
70
84
  if (formats.length) {
@@ -109,18 +123,26 @@ async function downloadAndSaveVariant(
109
123
  format: SupportedFormat,
110
124
  status: string | undefined,
111
125
  richText: boolean | undefined,
112
- token?: Token
126
+ token?: Token,
113
127
  ) {
114
128
  const api = createApiClient();
115
129
  const params: Record<string, string | null> = { variant: variantApiId };
116
130
  if (format) params.format = format;
117
- if (status) params.status = status;
118
131
  if (richText) params.includeRichText = richText.toString();
119
132
 
133
+ // Root-level status gets set as the default if specified
134
+ if (status) params.status = status;
135
+
120
136
  const savedMessages = await Promise.all(
121
- projects.map(async ({ id, fileName }: Project) => {
122
- const { data } = await api.get(`/projects/${id}`, {
123
- params,
137
+ projects.map(async (project) => {
138
+ const projectParams = { ...params };
139
+ // If project-level status is specified, overrides root-level status
140
+ if (project.status) projectParams.status = project.status;
141
+ if (project.exclude_components)
142
+ projectParams.exclude_components = String(project.exclude_components);
143
+
144
+ const { data } = await api.get(`/projects/${project.id}`, {
145
+ params: projectParams,
124
146
  headers: { Authorization: `token ${token}` },
125
147
  });
126
148
 
@@ -131,7 +153,7 @@ async function downloadAndSaveVariant(
131
153
  const extension = getFormatExtension(format);
132
154
 
133
155
  const filename = cleanFileName(
134
- fileName + ("__" + (variantApiId || "base")) + extension
156
+ project.fileName + ("__" + (variantApiId || "base")) + extension,
135
157
  );
136
158
  const filepath = path.join(consts.TEXT_DIR, filename);
137
159
 
@@ -145,9 +167,9 @@ async function downloadAndSaveVariant(
145
167
  return "";
146
168
  }
147
169
 
148
- fs.writeFileSync(filepath, dataString);
170
+ await writeFile(filepath, dataString);
149
171
  return getSavedMessage(filename);
150
- })
172
+ }),
151
173
  );
152
174
 
153
175
  return savedMessages.join("");
@@ -159,12 +181,12 @@ async function downloadAndSaveVariants(
159
181
  format: SupportedFormat,
160
182
  status: string | undefined,
161
183
  richText: boolean | undefined,
162
- token?: Token
184
+ token?: Token,
163
185
  ) {
164
186
  const messages = await Promise.all([
165
187
  downloadAndSaveVariant(null, projects, format, status, richText, token),
166
188
  ...variants.map(({ apiID }: { apiID: string }) =>
167
- downloadAndSaveVariant(apiID, projects, format, status, richText, token)
189
+ downloadAndSaveVariant(apiID, projects, format, status, richText, token),
168
190
  ),
169
191
  ]);
170
192
 
@@ -177,23 +199,31 @@ async function downloadAndSaveBase(
177
199
  status: string | undefined,
178
200
  richText?: boolean | undefined,
179
201
  token?: Token,
180
- options?: PullOptions
202
+ options?: PullOptions,
181
203
  ) {
182
204
  const api = createApiClient();
183
205
  const params = { ...options?.meta };
184
206
  if (format) params.format = format;
185
- if (status) params.status = status;
186
207
  if (richText) params.includeRichText = richText.toString();
187
208
 
209
+ // Root-level status gets set as the default if specified
210
+ if (status) params.status = status;
211
+
188
212
  const savedMessages = await Promise.all(
189
- projects.map(async ({ id, fileName }: Project) => {
190
- const { data } = await api.get(`/projects/${id}`, {
191
- params,
213
+ projects.map(async (project) => {
214
+ const projectParams = { ...params };
215
+ // If project-level status is specified, overrides root-level status
216
+ if (project.status) projectParams.status = project.status;
217
+ if (project.exclude_components)
218
+ projectParams.exclude_components = String(project.exclude_components);
219
+
220
+ const { data } = await api.get(`/projects/${project.id}`, {
221
+ params: projectParams,
192
222
  headers: { Authorization: `token ${token}` },
193
223
  });
194
224
 
195
225
  const extension = getFormatExtension(format);
196
- const filename = cleanFileName(`${fileName}__base${extension}`);
226
+ const filename = cleanFileName(`${project.fileName}__base${extension}`);
197
227
  const filepath = path.join(consts.TEXT_DIR, filename);
198
228
 
199
229
  let dataString = data;
@@ -206,9 +236,9 @@ async function downloadAndSaveBase(
206
236
  return "";
207
237
  }
208
238
 
209
- fs.writeFileSync(filepath, dataString);
239
+ await writeFile(filepath, dataString);
210
240
  return getSavedMessage(filename);
211
- })
241
+ }),
212
242
  );
213
243
 
214
244
  return savedMessages.join("");
@@ -236,16 +266,27 @@ function cleanOutputFiles() {
236
266
  async function downloadAndSave(
237
267
  source: SourceInformation,
238
268
  token?: Token,
239
- options?: PullOptions
269
+ options?: PullOptions,
240
270
  ) {
241
271
  const api = createApiClient();
272
+
273
+ if (process.env.DEBUG_CLI) {
274
+ try {
275
+ await api.get("/health");
276
+ console.debug("Can connect to api.dittowords.com");
277
+ } catch {
278
+ console.debug("CANNOT connect to api.dittowords.com");
279
+ }
280
+ }
281
+
242
282
  const {
243
283
  validProjects,
244
284
  format: formatFromSource,
245
285
  shouldFetchComponentLibrary,
246
286
  status,
247
287
  richText,
248
- componentFolders,
288
+ componentFolders: specifiedComponentFolders,
289
+ componentRoot,
249
290
  } = source;
250
291
 
251
292
  const formats = getFormat(formatFromSource);
@@ -254,17 +295,81 @@ async function downloadAndSave(
254
295
  const spinner = ora(msg);
255
296
  spinner.start();
256
297
 
257
- const variants = await fetchVariants(source);
298
+ const [variants, allComponentFoldersResponse] = await Promise.all([
299
+ fetchVariants(source),
300
+ fetchComponentFolders(),
301
+ ]);
302
+
303
+ const allComponentFolders = Object.entries(
304
+ allComponentFoldersResponse,
305
+ ).reduce(
306
+ (acc, [id, name]) => acc.concat([{ id, name }]),
307
+ [] as ComponentFolder[],
308
+ );
258
309
 
259
310
  try {
260
311
  msg += cleanOutputFiles();
261
312
  msg += `\nFetching the latest text from ${sourcesToText(
262
313
  validProjects,
263
- shouldFetchComponentLibrary
314
+ shouldFetchComponentLibrary,
264
315
  )}\n`;
265
316
 
266
317
  const meta = options ? options.meta : {};
267
318
 
319
+ const rootRequest = {
320
+ id: "__root__",
321
+ name: "Root",
322
+ // componentRoot can be a boolean or an object
323
+ status:
324
+ typeof source.componentRoot === "object"
325
+ ? source.componentRoot.status
326
+ : undefined,
327
+ };
328
+
329
+ let componentFolderRequests: ComponentFolder[] = [];
330
+
331
+ // there's a lot of complex logic here, and it's tempting to want to
332
+ // simplify it. however, it's difficult to get rid of the complexity
333
+ // without sacrificing specificity and expressiveness.
334
+ //
335
+ // if folders specified..
336
+ if (specifiedComponentFolders) {
337
+ switch (componentRoot) {
338
+ // .. and no root specified, you only get components in the specified folders
339
+ case undefined:
340
+ case false:
341
+ componentFolderRequests.push(...specifiedComponentFolders);
342
+ break;
343
+ // .. and root specified, you get components in folders and the root
344
+ default:
345
+ componentFolderRequests.push(...specifiedComponentFolders);
346
+ componentFolderRequests.push(rootRequest);
347
+ break;
348
+ }
349
+ }
350
+ // if no folders specified..
351
+ else {
352
+ switch (componentRoot) {
353
+ // .. and no root specified, you get all components including those in folders
354
+ case undefined:
355
+ componentFolderRequests.push(...allComponentFolders);
356
+ componentFolderRequests.push(rootRequest);
357
+ break;
358
+ // .. and root specified as false, you only get components in folders
359
+ case false:
360
+ componentFolderRequests.push(...allComponentFolders);
361
+ break;
362
+ // .. and root specified as true or config object, you only get components in the root
363
+ default:
364
+ componentFolderRequests.push(rootRequest);
365
+ break;
366
+ }
367
+ }
368
+
369
+ // this array is populated while fetching from the component library and is used when
370
+ // generating the index.js driver file
371
+ const componentSources: ComponentSource[] = [];
372
+
268
373
  async function fetchComponentLibrary(format: SupportedFormat) {
269
374
  // Always include a variant with an apiID of undefined to ensure that we
270
375
  // fetch the base text for the component library.
@@ -274,41 +379,70 @@ async function downloadAndSave(
274
379
  if (options?.meta)
275
380
  Object.entries(options.meta).forEach(([k, v]) => params.append(k, v));
276
381
  if (format) params.append("format", format);
277
- if (status) params.append("status", status);
278
382
  if (richText) params.append("includeRichText", richText.toString());
279
- if (componentFolders) {
280
- componentFolders.forEach(({ id }) => params.append("folder_id[]", id));
281
- }
282
383
 
283
- const messages = await Promise.all(
284
- componentVariants.map(async ({ apiID: variantApiId }) => {
285
- const p = new URLSearchParams(params);
286
- if (variantApiId) p.append("variant", variantApiId);
384
+ // Root-level status gets set as the default if specified
385
+ if (status) params.append("status", status);
287
386
 
288
- const { data } = await api.get(`/components`, { params: p });
387
+ const messagePromises: Promise<string>[] = [];
289
388
 
290
- const nameExt = getFormatExtension(format);
291
- const nameBase = "ditto-component-library";
292
- const namePostfix = `__${variantApiId || "base"}`;
389
+ componentVariants.forEach(({ apiID: variantApiId }) => {
390
+ messagePromises.push(
391
+ ...componentFolderRequests.map(async (componentFolder) => {
392
+ const componentFolderParams = new URLSearchParams(params);
293
393
 
294
- const fileName = cleanFileName(`${nameBase}${namePostfix}${nameExt}`);
295
- const filePath = path.join(consts.TEXT_DIR, fileName);
394
+ if (variantApiId)
395
+ componentFolderParams.append("variant", variantApiId);
296
396
 
297
- let dataString = data;
298
- if (nameExt === ".json") {
299
- dataString = JSON.stringify(data, null, 2);
300
- }
397
+ // If folder-level status is specified, overrides root-level status
398
+ if (componentFolder.status)
399
+ componentFolderParams.append("status", componentFolder.status);
301
400
 
302
- const dataIsValid = getFormatDataIsValid[format];
303
- if (!dataIsValid(dataString)) {
304
- return "";
305
- }
401
+ const url =
402
+ componentFolder.id === "__root__"
403
+ ? "/components?root_only=true"
404
+ : `/component-folders/${componentFolder.id}/components`;
306
405
 
307
- await new Promise((r) => fs.writeFile(filePath, dataString, r));
308
- return getSavedMessage(fileName);
309
- })
310
- );
406
+ const { data } = await api.get(url, {
407
+ params: componentFolderParams,
408
+ });
409
+
410
+ const nameExt = getFormatExtension(format);
411
+ const nameBase = "components";
412
+ const nameFolder = `__${componentFolder.name}`;
413
+ const namePostfix = `__${variantApiId || "base"}`;
414
+
415
+ const fileName = cleanFileName(
416
+ `${nameBase}${nameFolder}${namePostfix}${nameExt}`,
417
+ );
418
+ const filePath = path.join(consts.TEXT_DIR, fileName);
419
+
420
+ let dataString = data;
421
+ if (nameExt === ".json") {
422
+ dataString = JSON.stringify(data, null, 2);
423
+ }
424
+
425
+ const dataIsValid = getFormatDataIsValid[format];
426
+ if (!dataIsValid(dataString)) {
427
+ return "";
428
+ }
311
429
 
430
+ await writeFile(filePath, dataString);
431
+
432
+ componentSources.push({
433
+ type: "components",
434
+ id: "ditto_component_library",
435
+ name: "ditto_component_library",
436
+ fileName,
437
+ variant: variantApiId || "base",
438
+ });
439
+
440
+ return getSavedMessage(fileName);
441
+ }),
442
+ );
443
+ });
444
+
445
+ const messages = await Promise.all(messagePromises);
312
446
  msg += messages.join("");
313
447
  }
314
448
 
@@ -326,7 +460,7 @@ async function downloadAndSave(
326
460
  format,
327
461
  status,
328
462
  richText,
329
- token
463
+ token,
330
464
  )
331
465
  : await downloadAndSaveBase(
332
466
  validProjects,
@@ -336,7 +470,7 @@ async function downloadAndSave(
336
470
  token,
337
471
  {
338
472
  meta,
339
- }
473
+ },
340
474
  );
341
475
  }
342
476
 
@@ -346,14 +480,7 @@ async function downloadAndSave(
346
480
  }
347
481
  }
348
482
 
349
- const sources = [...validProjects];
350
- if (shouldFetchComponentLibrary) {
351
- sources.push({
352
- id: "ditto_component_library",
353
- name: "Ditto Component Library",
354
- fileName: "ditto-component-library",
355
- });
356
- }
483
+ const sources: Source[] = [...validProjects, ...componentSources];
357
484
 
358
485
  if (formats.some((f) => JSON_FORMATS.includes(f)))
359
486
  msg += generateJsDriver(sources);
@@ -375,7 +502,7 @@ async function downloadAndSave(
375
502
  if (e.response && e.response.status === 401) {
376
503
  error = "You don't have access to the selected projects";
377
504
  msg = `${output.errorText(error)}.\nChoose others using the ${output.info(
378
- "project"
505
+ "project",
379
506
  )} command, or update your API key.`;
380
507
  return console.log(msg);
381
508
  }
@@ -383,11 +510,11 @@ async function downloadAndSave(
383
510
  error =
384
511
  "One or more of the requested projects don't have Developer Mode enabled";
385
512
  msg = `${output.errorText(
386
- error
513
+ error,
387
514
  )}.\nPlease choose different projects using the ${output.info(
388
- "project"
515
+ "project",
389
516
  )} command, or turn on Developer Mode for all selected projects. Learn more here: ${output.subtle(
390
- "https://www.dittowords.com/docs/ditto-developer-mode"
517
+ "https://www.dittowords.com/docs/ditto-developer-mode",
391
518
  )}.`;
392
519
  return console.log(msg);
393
520
  }
@@ -395,7 +522,7 @@ async function downloadAndSave(
395
522
  error = "projects not found";
396
523
  }
397
524
  msg = `We hit an error fetching text from the projects: ${output.errorText(
398
- error
525
+ error,
399
526
  )}.\nChoose others using the ${output.info("project")} command.`;
400
527
  return console.log(msg);
401
528
  }
@@ -410,22 +537,27 @@ export const pull = async (options?: PullOptions) => {
410
537
  const token = config.getToken(consts.CONFIG_FILE, consts.API_HOST);
411
538
  const sourceInformation = config.parseSourceInformation();
412
539
 
540
+ if (process.env.DEBUG_CLI === "true") {
541
+ console.debug(`Token: ${token}`);
542
+ }
543
+
413
544
  try {
414
545
  return await downloadAndSave(sourceInformation, token, { meta });
415
546
  } catch (e) {
416
- Sentry.captureException(e);
547
+ const eventId = Sentry.captureException(e);
548
+ const eventStr = `\n\nError ID: ${output.info(eventId)}`;
417
549
  if (e instanceof AxiosError) {
418
550
  return quit(
419
551
  output.errorText(
420
- "Something went wrong connecting to Ditto servers. Please contact support or try again later."
421
- )
552
+ "Something went wrong connecting to Ditto servers. Please contact support or try again later.",
553
+ ) + eventStr,
422
554
  );
423
555
  }
424
556
 
425
557
  return quit(
426
558
  output.errorText(
427
- "Something went wrong. Please contact support or try again later."
428
- )
559
+ "Something went wrong. Please contact support or try again later.",
560
+ ) + eventStr,
429
561
  );
430
562
  }
431
563
  };
package/lib/types.ts CHANGED
@@ -3,13 +3,22 @@ export interface Project {
3
3
  id: string;
4
4
  url?: string;
5
5
  fileName?: string;
6
+ status?: string;
7
+ exclude_components?: boolean;
6
8
  }
7
9
 
8
- export type Source = Project;
10
+ export type ComponentSource = ComponentFolder & {
11
+ type: "components";
12
+ fileName: string;
13
+ variant: string;
14
+ };
15
+
16
+ export type Source = (Project & { type?: undefined }) | ComponentSource;
9
17
 
10
- interface ComponentFolder {
18
+ export interface ComponentFolder {
11
19
  id: string;
12
20
  name: string;
21
+ status?: string;
13
22
  }
14
23
 
15
24
  export type SupportedFormat =
@@ -20,12 +29,16 @@ export type SupportedFormat =
20
29
  | "ios-stringsdict"
21
30
  | "icu";
22
31
 
32
+ type ComponentsSourceBool = boolean;
33
+ type ComponentsSourceConfig = {
34
+ root?: boolean | { status: string };
35
+ folders?: ComponentFolder[];
36
+ };
37
+ type ComponentsSource = ComponentsSourceBool | ComponentsSourceConfig;
38
+
23
39
  export interface ConfigYAML {
24
40
  sources?: {
25
- components?: {
26
- enabled?: boolean;
27
- folders?: ComponentFolder[];
28
- };
41
+ components?: ComponentsSource;
29
42
  projects?: Project[];
30
43
  };
31
44
  format?: SupportedFormat;
@@ -42,13 +55,17 @@ export interface ConfigYAML {
42
55
 
43
56
  export interface SourceInformation {
44
57
  hasSourceData: boolean;
58
+ hasTopLevelProjectsField: boolean;
59
+ hasTopLevelComponentsField: boolean;
60
+ hasComponentLibraryInProjects: boolean;
45
61
  validProjects: Project[];
46
62
  shouldFetchComponentLibrary: boolean;
47
63
  variants: boolean;
48
64
  format: string | string[] | undefined;
49
65
  status: string | undefined;
50
66
  richText: boolean | undefined;
51
- componentFolders: ComponentFolder[] | null;
67
+ componentRoot: boolean | { status: string } | undefined;
68
+ componentFolders: ComponentFolder[] | undefined;
52
69
  }
53
70
 
54
71
  export type Token = string | undefined;
@@ -24,10 +24,6 @@ const stringifySourceId = (projectId: string) =>
24
24
 
25
25
  // TODO: support ESM
26
26
  export function generateJsDriver(sources: Source[]) {
27
- const fileNames = fs
28
- .readdirSync(consts.TEXT_DIR)
29
- .filter((fileName) => /\.json$/.test(fileName));
30
-
31
27
  const sourceIdsByName: Record<string, string> = sources.reduce(
32
28
  (agg, source) => {
33
29
  if (source.fileName) {
@@ -39,7 +35,14 @@ export function generateJsDriver(sources: Source[]) {
39
35
  {}
40
36
  );
41
37
 
42
- const data = fileNames.reduce(
38
+ const projectFileNames = fs
39
+ .readdirSync(consts.TEXT_DIR)
40
+ .filter(
41
+ (fileName) => /\.json$/.test(fileName) && !/^components__/.test(fileName)
42
+ );
43
+
44
+ type DriverFile = Record<string, Record<string, string | object>>;
45
+ const data: DriverFile = projectFileNames.reduce(
43
46
  (obj: Record<string, Record<string, string>>, fileName) => {
44
47
  const [sourceId, rest] = fileName.split("__");
45
48
  const [variantApiId] = rest.split(".");
@@ -57,9 +60,40 @@ export function generateJsDriver(sources: Source[]) {
57
60
  {}
58
61
  );
59
62
 
63
+ // Create arrays of stringified "...require()" statements,
64
+ // each of which corresponds to one of the component files
65
+ // (which are created on a per-component-folder basis)
66
+ const componentData: Record<string, string[]> = {};
67
+ sources
68
+ .filter((s) => s.type === "components")
69
+ .forEach((componentSource) => {
70
+ if (componentSource.type !== "components") return;
71
+ componentData[componentSource.variant] ??= [];
72
+ componentData[componentSource.variant].push(
73
+ `...require('./${componentSource.fileName}')`
74
+ );
75
+ });
76
+ // Convert each array of stringified "...require()" statements
77
+ // into a unified string, and set it on the final data object
78
+ // that will be written to the driver file
79
+ Object.keys(componentData).forEach((key) => {
80
+ data.ditto_component_library ??= {};
81
+
82
+ let str = "{";
83
+ componentData[key].forEach((k, i) => {
84
+ str += k;
85
+ if (i < componentData[key].length - 1) str += ", ";
86
+ });
87
+ str += "}";
88
+ data.ditto_component_library[key] = str;
89
+ });
90
+
60
91
  let dataString = `module.exports = ${JSON.stringify(data, null, 2)}`
61
92
  // remove quotes around require statements
62
- .replace(/"require\((.*)\)"/g, "require($1)");
93
+ .replace(/"require\((.*)\)"/g, "require($1)")
94
+ // remove quotes around opening & closing curlies
95
+ .replace(/"\{/g, "{")
96
+ .replace(/\}"/g, "}");
63
97
 
64
98
  const filePath = path.resolve(consts.TEXT_DIR, "index.js");
65
99
  fs.writeFileSync(filePath, dataString, { encoding: "utf8" });
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@dittowords/cli",
3
- "version": "4.0.0-alpha.0",
3
+ "version": "4.0.1-alpha.0",
4
4
  "description": "Command Line Interface for Ditto (dittowords.com).",
5
5
  "license": "MIT",
6
6
  "main": "bin/index.js",
7
7
  "scripts": {
8
- "prepublish": "etsc && sentry-cli sourcemaps inject ./bin && npx sentry-cli sourcemaps upload ./bin --release=\"$(cat package.json | jq -r '.version')\"",
8
+ "prepublishOnly": "ENV=production etsc && sentry-cli sourcemaps inject ./bin && npx sentry-cli sourcemaps upload ./bin --release=\"$(cat package.json | jq -r '.version')\"",
9
9
  "prepare": "husky install",
10
10
  "start": "etsc && node bin/ditto.js",
11
11
  "sync": "etsc && node bin/ditto.js pull",