@dittowords/cli 4.0.1-alpha.0 → 4.1.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 (47) hide show
  1. package/README.md +27 -371
  2. package/bin/api.js +3 -15
  3. package/bin/api.js.map +1 -1
  4. package/bin/config.js +5 -3
  5. package/bin/config.js.map +1 -1
  6. package/bin/generate-swift-struct.js +6 -0
  7. package/bin/generate-swift-struct.js.map +1 -0
  8. package/bin/http/fetchComponentFolders.js +3 -3
  9. package/bin/http/fetchComponentFolders.js.map +1 -1
  10. package/bin/http/fetchComponents.js +13 -5
  11. package/bin/http/fetchComponents.js.map +1 -1
  12. package/bin/http/fetchVariants.js +3 -3
  13. package/bin/http/fetchVariants.js.map +1 -1
  14. package/bin/init/project.js +3 -3
  15. package/bin/init/project.js.map +1 -1
  16. package/bin/pull.js +82 -49
  17. package/bin/pull.js.map +1 -1
  18. package/bin/pull.test.js +26 -24
  19. package/bin/pull.test.js.map +1 -1
  20. package/bin/sentry-test.js.map +1 -0
  21. package/bin/types.js +2 -2
  22. package/bin/types.js.map +1 -1
  23. package/bin/utils/determineModuleType.js +80 -0
  24. package/bin/utils/determineModuleType.js.map +1 -0
  25. package/bin/utils/generateIOSBundles.js +147 -0
  26. package/bin/utils/generateIOSBundles.js.map +1 -0
  27. package/bin/utils/generateJsDriver.js +117 -58
  28. package/bin/utils/generateJsDriver.js.map +1 -1
  29. package/bin/utils/generateJsDriverTypeFile.js +105 -0
  30. package/bin/utils/generateJsDriverTypeFile.js.map +1 -0
  31. package/bin/utils/generateSwiftDriver.js +93 -0
  32. package/bin/utils/generateSwiftDriver.js.map +1 -0
  33. package/lib/api.ts +1 -17
  34. package/lib/config.ts +4 -0
  35. package/lib/http/fetchComponentFolders.ts +1 -1
  36. package/lib/http/fetchComponents.ts +14 -9
  37. package/lib/http/fetchVariants.ts +1 -1
  38. package/lib/init/project.ts +1 -1
  39. package/lib/pull.test.ts +24 -22
  40. package/lib/pull.ts +127 -90
  41. package/lib/types.ts +4 -0
  42. package/lib/utils/determineModuleType.ts +57 -0
  43. package/lib/utils/generateIOSBundles.ts +122 -0
  44. package/lib/utils/generateJsDriver.ts +156 -51
  45. package/lib/utils/generateJsDriverTypeFile.ts +75 -0
  46. package/lib/utils/generateSwiftDriver.ts +48 -0
  47. package/package.json +1 -1
package/lib/pull.ts CHANGED
@@ -25,11 +25,26 @@ import { fetchVariants } from "./http/fetchVariants";
25
25
  import { quit } from "./utils/quit";
26
26
  import { AxiosError } from "axios";
27
27
  import { fetchComponentFolders } from "./http/fetchComponentFolders";
28
+ import { generateSwiftDriver } from "./utils/generateSwiftDriver";
29
+ import { generateIOSBundles } from "./utils/generateIOSBundles";
30
+
31
+ interface IRequestOptions {
32
+ projects: Project[];
33
+ format: SupportedFormat;
34
+ status: string | undefined;
35
+ richText?: boolean | undefined;
36
+ token?: Token;
37
+ options?: PullOptions;
38
+ }
39
+
40
+ interface IRequestOptionsWithVariants extends IRequestOptions {
41
+ variants: { apiID: string }[];
42
+ }
28
43
 
29
44
  const ensureEndsWithNewLine = (str: string) =>
30
45
  str + (/[\r\n]$/.test(str) ? "" : "\n");
31
46
 
32
- const writeFile = (path: string, data: string) =>
47
+ export const writeFile = (path: string, data: string) =>
33
48
  new Promise((r) => fs.writeFile(path, ensureEndsWithNewLine(data), r));
34
49
 
35
50
  const SUPPORTED_FORMATS: SupportedFormat[] = [
@@ -41,7 +56,20 @@ const SUPPORTED_FORMATS: SupportedFormat[] = [
41
56
  "icu",
42
57
  ];
43
58
 
44
- const JSON_FORMATS: SupportedFormat[] = ["flat", "structured", "icu"];
59
+ export type JSONFormat = "flat" | "nested" | "structured" | "icu";
60
+
61
+ const IOS_FORMATS: SupportedFormat[] = ["ios-strings", "ios-stringsdict"];
62
+ const JSON_FORMATS: JSONFormat[] = ["flat", "structured", "icu"];
63
+
64
+ const getJsonFormat = (formats: string[]): JSONFormat => {
65
+ // edge case: multiple json formats specified
66
+ // we should grab the last one
67
+ const jsonFormats = formats.filter((f) =>
68
+ JSON_FORMATS.includes(f as JSONFormat)
69
+ ) as JSONFormat[];
70
+
71
+ return jsonFormats[jsonFormats.length - 1] || "flat";
72
+ };
45
73
 
46
74
  const FORMAT_EXTENSIONS = {
47
75
  flat: ".json",
@@ -55,7 +83,7 @@ const FORMAT_EXTENSIONS = {
55
83
  const getJsonFormatIsValid = (data: string) => {
56
84
  try {
57
85
  return Object.keys(JSON.parse(data)).some(
58
- (k) => !k.startsWith("__variant"),
86
+ (k) => !k.startsWith("__variant")
59
87
  );
60
88
  } catch {
61
89
  return false;
@@ -73,12 +101,12 @@ export const getFormatDataIsValid = {
73
101
  };
74
102
 
75
103
  const getFormat = (
76
- formatFromSource: string | string[] | undefined,
104
+ formatFromSource: string | string[] | undefined
77
105
  ): SupportedFormat[] => {
78
106
  const formats = (
79
107
  Array.isArray(formatFromSource) ? formatFromSource : [formatFromSource]
80
108
  ).filter((format) =>
81
- SUPPORTED_FORMATS.includes(format as SupportedFormat),
109
+ SUPPORTED_FORMATS.includes(format as SupportedFormat)
82
110
  ) as SupportedFormat[];
83
111
 
84
112
  if (formats.length) {
@@ -119,12 +147,9 @@ async function askForAnotherToken() {
119
147
  */
120
148
  async function downloadAndSaveVariant(
121
149
  variantApiId: string | null,
122
- projects: Project[],
123
- format: SupportedFormat,
124
- status: string | undefined,
125
- richText: boolean | undefined,
126
- token?: Token,
150
+ requestOptions: IRequestOptions
127
151
  ) {
152
+ const { projects, format, status, richText, token } = requestOptions;
128
153
  const api = createApiClient();
129
154
  const params: Record<string, string | null> = { variant: variantApiId };
130
155
  if (format) params.format = format;
@@ -141,7 +166,7 @@ async function downloadAndSaveVariant(
141
166
  if (project.exclude_components)
142
167
  projectParams.exclude_components = String(project.exclude_components);
143
168
 
144
- const { data } = await api.get(`/projects/${project.id}`, {
169
+ const { data } = await api.get(`/v1/projects/${project.id}`, {
145
170
  params: projectParams,
146
171
  headers: { Authorization: `token ${token}` },
147
172
  });
@@ -153,7 +178,7 @@ async function downloadAndSaveVariant(
153
178
  const extension = getFormatExtension(format);
154
179
 
155
180
  const filename = cleanFileName(
156
- project.fileName + ("__" + (variantApiId || "base")) + extension,
181
+ project.fileName + ("__" + (variantApiId || "base")) + extension
157
182
  );
158
183
  const filepath = path.join(consts.TEXT_DIR, filename);
159
184
 
@@ -169,38 +194,28 @@ async function downloadAndSaveVariant(
169
194
 
170
195
  await writeFile(filepath, dataString);
171
196
  return getSavedMessage(filename);
172
- }),
197
+ })
173
198
  );
174
199
 
175
200
  return savedMessages.join("");
176
201
  }
177
202
 
178
203
  async function downloadAndSaveVariants(
179
- variants: { apiID: string }[],
180
- projects: Project[],
181
- format: SupportedFormat,
182
- status: string | undefined,
183
- richText: boolean | undefined,
184
- token?: Token,
204
+ requestOptions: IRequestOptionsWithVariants
185
205
  ) {
186
206
  const messages = await Promise.all([
187
- downloadAndSaveVariant(null, projects, format, status, richText, token),
188
- ...variants.map(({ apiID }: { apiID: string }) =>
189
- downloadAndSaveVariant(apiID, projects, format, status, richText, token),
207
+ downloadAndSaveVariant(null, requestOptions),
208
+ ...requestOptions.variants.map(({ apiID }: { apiID: string }) =>
209
+ downloadAndSaveVariant(apiID, requestOptions)
190
210
  ),
191
211
  ]);
192
212
 
193
213
  return messages.join("");
194
214
  }
195
215
 
196
- async function downloadAndSaveBase(
197
- projects: Project[],
198
- format: SupportedFormat,
199
- status: string | undefined,
200
- richText?: boolean | undefined,
201
- token?: Token,
202
- options?: PullOptions,
203
- ) {
216
+ async function downloadAndSaveBase(requestOptions: IRequestOptions) {
217
+ const { projects, format, status, richText, token, options } = requestOptions;
218
+
204
219
  const api = createApiClient();
205
220
  const params = { ...options?.meta };
206
221
  if (format) params.format = format;
@@ -217,7 +232,7 @@ async function downloadAndSaveBase(
217
232
  if (project.exclude_components)
218
233
  projectParams.exclude_components = String(project.exclude_components);
219
234
 
220
- const { data } = await api.get(`/projects/${project.id}`, {
235
+ const { data } = await api.get(`/v1/projects/${project.id}`, {
221
236
  params: projectParams,
222
237
  headers: { Authorization: `token ${token}` },
223
238
  });
@@ -238,7 +253,7 @@ async function downloadAndSaveBase(
238
253
 
239
254
  await writeFile(filepath, dataString);
240
255
  return getSavedMessage(filename);
241
- }),
256
+ })
242
257
  );
243
258
 
244
259
  return savedMessages.join("");
@@ -253,10 +268,23 @@ function cleanOutputFiles() {
253
268
  fs.mkdirSync(consts.TEXT_DIR);
254
269
  }
255
270
 
256
- const fileNames = fs.readdirSync(consts.TEXT_DIR);
257
- fileNames.forEach((fileName) => {
258
- if (/\.js(on)?|\.xml|\.strings(dict)?$/.test(fileName)) {
259
- fs.unlinkSync(path.resolve(consts.TEXT_DIR, fileName));
271
+ const directoryContents = fs.readdirSync(consts.TEXT_DIR, {
272
+ withFileTypes: true,
273
+ });
274
+
275
+ directoryContents.forEach((item) => {
276
+ if (item.isDirectory() && /\.lproj$/.test(item.name)) {
277
+ return fs.rmSync(path.resolve(consts.TEXT_DIR, item.name), {
278
+ recursive: true,
279
+ force: true,
280
+ });
281
+ }
282
+
283
+ if (
284
+ item.isFile() &&
285
+ /\.js(on)?|\.xml|\.strings(dict)?$|\.swift$/.test(item.name)
286
+ ) {
287
+ return fs.unlinkSync(path.resolve(consts.TEXT_DIR, item.name));
260
288
  }
261
289
  });
262
290
 
@@ -266,19 +294,9 @@ function cleanOutputFiles() {
266
294
  async function downloadAndSave(
267
295
  source: SourceInformation,
268
296
  token?: Token,
269
- options?: PullOptions,
297
+ options?: PullOptions
270
298
  ) {
271
299
  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
-
282
300
  const {
283
301
  validProjects,
284
302
  format: formatFromSource,
@@ -287,10 +305,19 @@ async function downloadAndSave(
287
305
  richText,
288
306
  componentFolders: specifiedComponentFolders,
289
307
  componentRoot,
308
+ localeByVariantApiId,
290
309
  } = source;
291
310
 
292
311
  const formats = getFormat(formatFromSource);
293
312
 
313
+ const hasJSONFormat = formats.some((f) =>
314
+ JSON_FORMATS.includes(f as JSONFormat)
315
+ );
316
+ const hasIOSFormat = formats.some((f) => IOS_FORMATS.includes(f));
317
+ const shouldGenerateIOSBundles = hasIOSFormat && localeByVariantApiId;
318
+
319
+ const shouldLogOutputFiles = !shouldGenerateIOSBundles;
320
+
294
321
  let msg = "";
295
322
  const spinner = ora(msg);
296
323
  spinner.start();
@@ -301,17 +328,17 @@ async function downloadAndSave(
301
328
  ]);
302
329
 
303
330
  const allComponentFolders = Object.entries(
304
- allComponentFoldersResponse,
331
+ allComponentFoldersResponse
305
332
  ).reduce(
306
333
  (acc, [id, name]) => acc.concat([{ id, name }]),
307
- [] as ComponentFolder[],
334
+ [] as ComponentFolder[]
308
335
  );
309
336
 
310
337
  try {
311
338
  msg += cleanOutputFiles();
312
339
  msg += `\nFetching the latest text from ${sourcesToText(
313
340
  validProjects,
314
- shouldFetchComponentLibrary,
341
+ shouldFetchComponentLibrary
315
342
  )}\n`;
316
343
 
317
344
  const meta = options ? options.meta : {};
@@ -400,8 +427,8 @@ async function downloadAndSave(
400
427
 
401
428
  const url =
402
429
  componentFolder.id === "__root__"
403
- ? "/components?root_only=true"
404
- : `/component-folders/${componentFolder.id}/components`;
430
+ ? "/v1/components?root_only=true"
431
+ : `/v1/component-folders/${componentFolder.id}/components`;
405
432
 
406
433
  const { data } = await api.get(url, {
407
434
  params: componentFolderParams,
@@ -413,7 +440,7 @@ async function downloadAndSave(
413
440
  const namePostfix = `__${variantApiId || "base"}`;
414
441
 
415
442
  const fileName = cleanFileName(
416
- `${nameBase}${nameFolder}${namePostfix}${nameExt}`,
443
+ `${nameBase}${nameFolder}${namePostfix}${nameExt}`
417
444
  );
418
445
  const filePath = path.join(consts.TEXT_DIR, fileName);
419
446
 
@@ -438,12 +465,14 @@ async function downloadAndSave(
438
465
  });
439
466
 
440
467
  return getSavedMessage(fileName);
441
- }),
468
+ })
442
469
  );
443
470
  });
444
471
 
445
472
  const messages = await Promise.all(messagePromises);
446
- msg += messages.join("");
473
+ if (shouldLogOutputFiles) {
474
+ msg += messages.join("");
475
+ }
447
476
  }
448
477
 
449
478
  if (shouldFetchComponentLibrary) {
@@ -453,25 +482,32 @@ async function downloadAndSave(
453
482
  }
454
483
 
455
484
  async function fetchProjects(format: SupportedFormat) {
456
- msg += variants
457
- ? await downloadAndSaveVariants(
458
- variants,
459
- validProjects,
460
- format,
461
- status,
462
- richText,
463
- token,
464
- )
465
- : await downloadAndSaveBase(
466
- validProjects,
467
- format,
468
- status,
469
- richText,
470
- token,
471
- {
472
- meta,
473
- },
474
- );
485
+ let result = "";
486
+ if (variants) {
487
+ result = await downloadAndSaveVariants({
488
+ variants,
489
+ projects: validProjects,
490
+ format,
491
+ status,
492
+ richText,
493
+ token,
494
+ });
495
+ } else {
496
+ result = await downloadAndSaveBase({
497
+ projects: validProjects,
498
+ format,
499
+ status,
500
+ richText,
501
+ token,
502
+ options: {
503
+ meta,
504
+ },
505
+ });
506
+ }
507
+
508
+ if (shouldLogOutputFiles) {
509
+ msg += result;
510
+ }
475
511
  }
476
512
 
477
513
  if (validProjects.length) {
@@ -482,10 +518,15 @@ async function downloadAndSave(
482
518
 
483
519
  const sources: Source[] = [...validProjects, ...componentSources];
484
520
 
485
- if (formats.some((f) => JSON_FORMATS.includes(f)))
486
- msg += generateJsDriver(sources);
521
+ if (hasJSONFormat) msg += generateJsDriver(sources, getJsonFormat(formats));
487
522
 
488
- msg += `\n${output.success("Done")}!`;
523
+ if (shouldGenerateIOSBundles) {
524
+ msg += "iOS locale information detected, generating bundles..\n\n";
525
+ msg += await generateIOSBundles(localeByVariantApiId);
526
+ msg += await generateSwiftDriver(source);
527
+ }
528
+
529
+ msg += `\n\n${output.success("Done")}!`;
489
530
 
490
531
  spinner.stop();
491
532
  return console.log(msg);
@@ -502,7 +543,7 @@ async function downloadAndSave(
502
543
  if (e.response && e.response.status === 401) {
503
544
  error = "You don't have access to the selected projects";
504
545
  msg = `${output.errorText(error)}.\nChoose others using the ${output.info(
505
- "project",
546
+ "project"
506
547
  )} command, or update your API key.`;
507
548
  return console.log(msg);
508
549
  }
@@ -510,11 +551,11 @@ async function downloadAndSave(
510
551
  error =
511
552
  "One or more of the requested projects don't have Developer Mode enabled";
512
553
  msg = `${output.errorText(
513
- error,
554
+ error
514
555
  )}.\nPlease choose different projects using the ${output.info(
515
- "project",
556
+ "project"
516
557
  )} command, or turn on Developer Mode for all selected projects. Learn more here: ${output.subtle(
517
- "https://www.dittowords.com/docs/ditto-developer-mode",
558
+ "https://www.dittowords.com/docs/ditto-developer-mode"
518
559
  )}.`;
519
560
  return console.log(msg);
520
561
  }
@@ -522,7 +563,7 @@ async function downloadAndSave(
522
563
  error = "projects not found";
523
564
  }
524
565
  msg = `We hit an error fetching text from the projects: ${output.errorText(
525
- error,
566
+ error
526
567
  )}.\nChoose others using the ${output.info("project")} command.`;
527
568
  return console.log(msg);
528
569
  }
@@ -537,10 +578,6 @@ export const pull = async (options?: PullOptions) => {
537
578
  const token = config.getToken(consts.CONFIG_FILE, consts.API_HOST);
538
579
  const sourceInformation = config.parseSourceInformation();
539
580
 
540
- if (process.env.DEBUG_CLI === "true") {
541
- console.debug(`Token: ${token}`);
542
- }
543
-
544
581
  try {
545
582
  return await downloadAndSave(sourceInformation, token, { meta });
546
583
  } catch (e) {
@@ -549,15 +586,15 @@ export const pull = async (options?: PullOptions) => {
549
586
  if (e instanceof AxiosError) {
550
587
  return quit(
551
588
  output.errorText(
552
- "Something went wrong connecting to Ditto servers. Please contact support or try again later.",
553
- ) + eventStr,
589
+ "Something went wrong connecting to Ditto servers. Please contact support or try again later."
590
+ ) + eventStr
554
591
  );
555
592
  }
556
593
 
557
594
  return quit(
558
595
  output.errorText(
559
- "Something went wrong. Please contact support or try again later.",
560
- ) + eventStr,
596
+ "Something went wrong. Please contact support or try again later."
597
+ ) + eventStr
561
598
  );
562
599
  }
563
600
  };
package/lib/types.ts CHANGED
@@ -46,6 +46,9 @@ export interface ConfigYAML {
46
46
  variants?: boolean;
47
47
  richText?: boolean;
48
48
 
49
+ // TODO: might want to rename this at some point
50
+ iosLocales?: Record<string, string>[];
51
+
49
52
  // these are legacy fields - if they exist, we should output
50
53
  // a deprecation error, and suggest that they nest them under
51
54
  // a top-level `sources` property
@@ -66,6 +69,7 @@ export interface SourceInformation {
66
69
  richText: boolean | undefined;
67
70
  componentRoot: boolean | { status: string } | undefined;
68
71
  componentFolders: ComponentFolder[] | undefined;
72
+ localeByVariantApiId: Record<string, string> | undefined;
69
73
  }
70
74
 
71
75
  export type Token = string | undefined;
@@ -0,0 +1,57 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+
4
+ export type ModuleType = "commonjs" | "module";
5
+
6
+ /**
7
+ * Looks for a `package.json` file starting in the current working directory and traversing upwards
8
+ * until it finds one or reaches root.
9
+ * @returns "commonjs" or "module", defaulting to "module" if no `package.json` is found or if the found
10
+ * file does not include a `type` property.
11
+ */
12
+ export function determineModuleType() {
13
+ const value = getRawTypeFromPackageJson();
14
+ return getTypeOrDefault(value);
15
+ }
16
+
17
+ function getRawTypeFromPackageJson() {
18
+ if (process.env.DITTO_MODULE_TYPE) {
19
+ return process.env.DITTO_MODULE_TYPE;
20
+ }
21
+
22
+ let currentDir: string | null = process.cwd(); // Get the current working directory
23
+
24
+ while (currentDir) {
25
+ const packageJsonPath = path.join(currentDir, "package.json");
26
+ if (fs.existsSync(packageJsonPath)) {
27
+ const packageJsonContents = fs.readFileSync(packageJsonPath, "utf8");
28
+ try {
29
+ const packageData: { type?: string } = JSON.parse(packageJsonContents);
30
+ if (packageData?.type) {
31
+ return packageData.type;
32
+ }
33
+ } catch {}
34
+
35
+ return null;
36
+ }
37
+
38
+ if (currentDir === "/") {
39
+ return null;
40
+ }
41
+
42
+ // Move up a directory and continue the search
43
+ currentDir = path.dirname(currentDir);
44
+ }
45
+
46
+ // No package.json
47
+ return null;
48
+ }
49
+
50
+ function getTypeOrDefault(value: string | null): ModuleType {
51
+ const valueLower = value?.toLowerCase() || "";
52
+ if (valueLower === "commonjs" || valueLower === "module") {
53
+ return valueLower;
54
+ }
55
+
56
+ return "commonjs";
57
+ }
@@ -0,0 +1,122 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import consts from "../consts";
4
+ import output from "../output";
5
+
6
+ const IOS_FILE_EXTENSION_PATTERN = /\.(strings|stringsdict)$/;
7
+
8
+ export async function generateIOSBundles(
9
+ localeByVariantApiId: Record<string, string> | undefined
10
+ ) {
11
+ const files = fs.readdirSync(consts.TEXT_DIR);
12
+
13
+ const bundlesGenerated: {
14
+ [bundleName: string]: {
15
+ mappedVariant?: string;
16
+ };
17
+ } = {};
18
+
19
+ for (const fileName of files) {
20
+ if (!IOS_FILE_EXTENSION_PATTERN.test(fileName)) {
21
+ continue;
22
+ }
23
+
24
+ const [name, fileExtension] = fileName.split(".");
25
+ if (!name.length) {
26
+ continue;
27
+ }
28
+
29
+ const parts = name.split("__");
30
+ const source = parts[0];
31
+ const variant = parts[parts.length - 1];
32
+ if (!(source && variant)) {
33
+ continue;
34
+ }
35
+
36
+ const bundleName =
37
+ localeByVariantApiId && localeByVariantApiId[variant]
38
+ ? localeByVariantApiId[variant]
39
+ : variant;
40
+ const bundleFileName = `${bundleName}.lproj`;
41
+ const bundleFolder = path.join(consts.TEXT_DIR, bundleFileName);
42
+ if (!fs.existsSync(bundleFolder)) {
43
+ fs.mkdirSync(bundleFolder);
44
+ }
45
+
46
+ const filePathCurrent = path.join(consts.TEXT_DIR, fileName);
47
+ const filePathNew = path.join(bundleFolder, `${source}.${fileExtension}`);
48
+
49
+ handleBundleGeneration(source, fileExtension, filePathCurrent, filePathNew);
50
+
51
+ bundlesGenerated[bundleFileName] = {
52
+ mappedVariant: variant === bundleName ? undefined : variant,
53
+ };
54
+ }
55
+
56
+ return (
57
+ Object.keys(bundlesGenerated)
58
+ .map((bundleName) => {
59
+ let msg = `Successfully generated iOS bundle ${output.info(
60
+ bundleName
61
+ )}`;
62
+ const mappedVariant = bundlesGenerated[bundleName].mappedVariant;
63
+ if (mappedVariant) {
64
+ msg += ` ${output.subtle(`(mapped to variant '${mappedVariant}')`)}`;
65
+ }
66
+ return msg;
67
+ })
68
+ .join("\n") + "\n"
69
+ );
70
+ }
71
+
72
+ function handleBundleGeneration(
73
+ sourceId: string,
74
+ extension: string,
75
+ sourcePath: string,
76
+ newFilePath: string
77
+ ) {
78
+ if (!fs.existsSync(newFilePath)) {
79
+ return fs.renameSync(sourcePath, newFilePath);
80
+ }
81
+
82
+ if (sourceId !== "components") {
83
+ throw new Error("Bundle path for " + sourceId + " already exists");
84
+ }
85
+
86
+ if (extension === "strings") {
87
+ return appendStringsFile(sourcePath, newFilePath);
88
+ }
89
+
90
+ if (extension === "stringsdict") {
91
+ return appendStringsDictFile(sourcePath, newFilePath);
92
+ }
93
+
94
+ throw new Error("Unsupported extension " + extension);
95
+ }
96
+
97
+ function appendStringsFile(sourcePath: string, destPath: string) {
98
+ const sourceContents = fs.readFileSync(sourcePath, "utf-8");
99
+ const newFileContents = fs.readFileSync(destPath, "utf-8");
100
+ const newContents = newFileContents + "\n" + sourceContents;
101
+ fs.writeFileSync(destPath, newContents);
102
+ fs.unlinkSync(sourcePath);
103
+ }
104
+
105
+ function appendStringsDictFile(sourcePath: string, destPath: string) {
106
+ const sourceContentsFull = fs.readFileSync(sourcePath, "utf-8");
107
+ const sourceContentsContent = sourceContentsFull.split("\n").slice(3, -4);
108
+
109
+ const newFileContentsFull = fs.readFileSync(destPath, "utf-8");
110
+ const newFileContentsContent = newFileContentsFull.split("\n").slice(3, -4);
111
+
112
+ const newContents = `<?xml version="1.0" encoding="utf-8"?>
113
+ <plist version="1.0">
114
+ <dict>
115
+ ${[newFileContentsContent, sourceContentsContent].join("\n")}
116
+ </dict>
117
+ </plist>
118
+ `;
119
+
120
+ fs.writeFileSync(destPath, newContents);
121
+ fs.unlinkSync(sourcePath);
122
+ }