@dittowords/cli 4.0.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 (43) hide show
  1. package/README.md +29 -364
  2. package/bin/config.js +5 -3
  3. package/bin/config.js.map +1 -1
  4. package/bin/generate-swift-struct.js +6 -0
  5. package/bin/generate-swift-struct.js.map +1 -0
  6. package/bin/http/fetchComponentFolders.js +3 -3
  7. package/bin/http/fetchComponentFolders.js.map +1 -1
  8. package/bin/http/fetchComponents.js +13 -5
  9. package/bin/http/fetchComponents.js.map +1 -1
  10. package/bin/http/fetchVariants.js +3 -3
  11. package/bin/http/fetchVariants.js.map +1 -1
  12. package/bin/init/project.js +3 -3
  13. package/bin/init/project.js.map +1 -1
  14. package/bin/pull.js +82 -38
  15. package/bin/pull.js.map +1 -1
  16. package/bin/pull.test.js +26 -24
  17. package/bin/pull.test.js.map +1 -1
  18. package/bin/types.js +2 -2
  19. package/bin/types.js.map +1 -1
  20. package/bin/utils/determineModuleType.js +80 -0
  21. package/bin/utils/determineModuleType.js.map +1 -0
  22. package/bin/utils/generateIOSBundles.js +147 -0
  23. package/bin/utils/generateIOSBundles.js.map +1 -0
  24. package/bin/utils/generateJsDriver.js +117 -58
  25. package/bin/utils/generateJsDriver.js.map +1 -1
  26. package/bin/utils/generateJsDriverTypeFile.js +105 -0
  27. package/bin/utils/generateJsDriverTypeFile.js.map +1 -0
  28. package/bin/utils/generateSwiftDriver.js +93 -0
  29. package/bin/utils/generateSwiftDriver.js.map +1 -0
  30. package/lib/config.ts +4 -0
  31. package/lib/http/fetchComponentFolders.ts +1 -1
  32. package/lib/http/fetchComponents.ts +14 -9
  33. package/lib/http/fetchVariants.ts +1 -1
  34. package/lib/init/project.ts +1 -1
  35. package/lib/pull.test.ts +24 -22
  36. package/lib/pull.ts +106 -55
  37. package/lib/types.ts +4 -0
  38. package/lib/utils/determineModuleType.ts +57 -0
  39. package/lib/utils/generateIOSBundles.ts +122 -0
  40. package/lib/utils/generateJsDriver.ts +156 -51
  41. package/lib/utils/generateJsDriverTypeFile.ts +75 -0
  42. package/lib/utils/generateSwiftDriver.ts +48 -0
  43. 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",
@@ -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
  });
@@ -176,31 +201,21 @@ async function downloadAndSaveVariant(
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
  });
@@ -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
 
@@ -277,10 +305,19 @@ async function downloadAndSave(
277
305
  richText,
278
306
  componentFolders: specifiedComponentFolders,
279
307
  componentRoot,
308
+ localeByVariantApiId,
280
309
  } = source;
281
310
 
282
311
  const formats = getFormat(formatFromSource);
283
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
+
284
321
  let msg = "";
285
322
  const spinner = ora(msg);
286
323
  spinner.start();
@@ -390,8 +427,8 @@ async function downloadAndSave(
390
427
 
391
428
  const url =
392
429
  componentFolder.id === "__root__"
393
- ? "/components?root_only=true"
394
- : `/component-folders/${componentFolder.id}/components`;
430
+ ? "/v1/components?root_only=true"
431
+ : `/v1/component-folders/${componentFolder.id}/components`;
395
432
 
396
433
  const { data } = await api.get(url, {
397
434
  params: componentFolderParams,
@@ -433,7 +470,9 @@ async function downloadAndSave(
433
470
  });
434
471
 
435
472
  const messages = await Promise.all(messagePromises);
436
- msg += messages.join("");
473
+ if (shouldLogOutputFiles) {
474
+ msg += messages.join("");
475
+ }
437
476
  }
438
477
 
439
478
  if (shouldFetchComponentLibrary) {
@@ -443,25 +482,32 @@ async function downloadAndSave(
443
482
  }
444
483
 
445
484
  async function fetchProjects(format: SupportedFormat) {
446
- msg += variants
447
- ? await downloadAndSaveVariants(
448
- variants,
449
- validProjects,
450
- format,
451
- status,
452
- richText,
453
- token
454
- )
455
- : await downloadAndSaveBase(
456
- validProjects,
457
- format,
458
- status,
459
- richText,
460
- token,
461
- {
462
- meta,
463
- }
464
- );
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
+ }
465
511
  }
466
512
 
467
513
  if (validProjects.length) {
@@ -472,10 +518,15 @@ async function downloadAndSave(
472
518
 
473
519
  const sources: Source[] = [...validProjects, ...componentSources];
474
520
 
475
- if (formats.some((f) => JSON_FORMATS.includes(f)))
476
- msg += generateJsDriver(sources);
521
+ if (hasJSONFormat) msg += generateJsDriver(sources, getJsonFormat(formats));
522
+
523
+ if (shouldGenerateIOSBundles) {
524
+ msg += "iOS locale information detected, generating bundles..\n\n";
525
+ msg += await generateIOSBundles(localeByVariantApiId);
526
+ msg += await generateSwiftDriver(source);
527
+ }
477
528
 
478
- msg += `\n${output.success("Done")}!`;
529
+ msg += `\n\n${output.success("Done")}!`;
479
530
 
480
531
  spinner.stop();
481
532
  return console.log(msg);
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
+ }