@dittowords/cli 4.3.0 → 4.4.1

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 (85) hide show
  1. package/.github/actions/install-node-dependencies/action.yml +24 -0
  2. package/.github/workflows/required-checks.yml +24 -0
  3. package/.sentryclirc +3 -0
  4. package/__mocks__/fs.js +2 -0
  5. package/bin/__mocks__/api.js +48 -0
  6. package/bin/__mocks__/api.js.map +1 -0
  7. package/bin/config.test.js +4 -3
  8. package/bin/config.test.js.map +1 -1
  9. package/bin/consts.js +19 -29
  10. package/bin/consts.js.map +1 -1
  11. package/bin/ditto.js +12 -4
  12. package/bin/ditto.js.map +1 -1
  13. package/bin/generate-suggestions.js +68 -58
  14. package/bin/generate-suggestions.js.map +1 -1
  15. package/bin/generate-suggestions.test.js +24 -13
  16. package/bin/generate-suggestions.test.js.map +1 -1
  17. package/bin/http/__mocks__/fetchComponentFolders.js +71 -0
  18. package/bin/http/__mocks__/fetchComponentFolders.js.map +1 -0
  19. package/bin/http/__mocks__/fetchComponents.js +73 -0
  20. package/bin/http/__mocks__/fetchComponents.js.map +1 -0
  21. package/bin/http/__mocks__/fetchVariants.js +71 -0
  22. package/bin/http/__mocks__/fetchVariants.js.map +1 -0
  23. package/bin/http/fetchComponentFolders.js +4 -4
  24. package/bin/http/fetchComponentFolders.js.map +1 -1
  25. package/bin/http/fetchComponents.js +4 -4
  26. package/bin/http/fetchComponents.js.map +1 -1
  27. package/bin/http/fetchVariants.js +6 -3
  28. package/bin/http/fetchVariants.js.map +1 -1
  29. package/bin/http/http.test.js +159 -0
  30. package/bin/http/http.test.js.map +1 -0
  31. package/bin/http/importComponents.js +9 -2
  32. package/bin/http/importComponents.js.map +1 -1
  33. package/bin/init/project.test.js +5 -28
  34. package/bin/init/project.test.js.map +1 -1
  35. package/bin/init/token.js +72 -27
  36. package/bin/init/token.js.map +1 -1
  37. package/bin/init/token.test.js +87 -9
  38. package/bin/init/token.test.js.map +1 -1
  39. package/bin/pull-lib.test.js +379 -0
  40. package/bin/pull-lib.test.js.map +1 -0
  41. package/bin/pull.js +15 -4
  42. package/bin/pull.js.map +1 -1
  43. package/bin/pull.test.js +73 -290
  44. package/bin/pull.test.js.map +1 -1
  45. package/bin/replace.js +22 -6
  46. package/bin/replace.js.map +1 -1
  47. package/bin/replace.test.js +53 -11
  48. package/bin/replace.test.js.map +1 -1
  49. package/bin/utils/determineModuleType.js +6 -7
  50. package/bin/utils/determineModuleType.js.map +1 -1
  51. package/bin/utils/determineModuleType.test.js +60 -0
  52. package/bin/utils/determineModuleType.test.js.map +1 -0
  53. package/bin/utils/getSelectedProjects.js +5 -5
  54. package/bin/utils/getSelectedProjects.js.map +1 -1
  55. package/bin/utils/quit.js +3 -3
  56. package/bin/utils/quit.js.map +1 -1
  57. package/jest.config.ts +16 -0
  58. package/lib/__mocks__/api.ts +12 -0
  59. package/lib/config.test.ts +3 -1
  60. package/lib/consts.ts +19 -17
  61. package/lib/ditto.ts +9 -1
  62. package/lib/generate-suggestions.test.ts +23 -11
  63. package/lib/generate-suggestions.ts +89 -79
  64. package/lib/http/__mocks__/fetchComponentFolders.ts +23 -0
  65. package/lib/http/__mocks__/fetchComponents.ts +24 -0
  66. package/lib/http/__mocks__/fetchVariants.ts +21 -0
  67. package/lib/http/fetchComponentFolders.ts +6 -4
  68. package/lib/http/fetchComponents.ts +5 -3
  69. package/lib/http/fetchVariants.ts +15 -4
  70. package/lib/http/http.test.ts +122 -0
  71. package/lib/http/importComponents.ts +8 -0
  72. package/lib/init/project.test.ts +4 -27
  73. package/lib/init/token.test.ts +55 -7
  74. package/lib/init/token.ts +76 -27
  75. package/lib/pull-lib.test.ts +367 -0
  76. package/lib/pull.test.ts +63 -316
  77. package/lib/pull.ts +13 -2
  78. package/lib/replace.test.ts +46 -10
  79. package/lib/replace.ts +20 -3
  80. package/lib/utils/determineModuleType.test.ts +48 -0
  81. package/lib/utils/determineModuleType.ts +4 -6
  82. package/lib/utils/getSelectedProjects.ts +3 -3
  83. package/lib/utils/quit.ts +1 -1
  84. package/package.json +4 -3
  85. package/jest.config.js +0 -6
package/lib/pull.test.ts CHANGED
@@ -1,330 +1,77 @@
1
- import fs from "fs";
2
- import path from "path";
3
-
4
- jest.mock("./api", () => ({
5
- createApiClient: jest.fn(), // this needs to be mocked in each test that requires it
6
- }));
7
- import { createApiClient } from "./api";
8
-
9
- const testProjects: Project[] = [
10
- {
11
- id: "1",
12
- name: "Project 1",
13
- fileName: "Project 1",
14
- },
15
- { id: "2", name: "Project 2", fileName: "Project 2" },
16
- ];
17
-
18
- // TODO: all tests in this file currently failing because we're re-instantiating the api client
19
- // everywhere and are unable to mock the return type separately for each instance of usage.
20
- // We need to refactor to share one api client everywhere instead of always re-creating it.
21
- const mockApi = createApiClient() as any as jest.Mocked<
22
- ReturnType<typeof createApiClient>
23
- >;
24
-
25
- jest.mock("./consts", () => ({
26
- TEXT_DIR: ".testing",
27
- API_HOST: "https://api.dittowords.com",
28
- CONFIG_FILE: ".testing/ditto",
29
- PROJECT_CONFIG_FILE: ".testing/config.yml",
30
- TEXT_FILE: ".testing/text.json",
31
- }));
32
-
1
+ import { pull } from "./pull";
2
+ import { vol } from "memfs";
33
3
  import consts from "./consts";
34
- import allPull, { getFormatDataIsValid } from "./pull";
35
- import { Project, SupportedExtension, SupportedFormat } from "./types";
36
-
37
- const {
38
- _testing: { cleanOutputFiles, downloadAndSaveVariant, downloadAndSaveBase },
39
- } = allPull;
40
- const variant = "english";
41
-
42
- const cleanOutputDir = () => {
43
- if (fs.existsSync(consts.TEXT_DIR))
44
- fs.rmSync(consts.TEXT_DIR, { recursive: true, force: true });
45
-
46
- fs.mkdirSync(consts.TEXT_DIR);
47
- };
48
-
49
- afterAll(() => {
50
- fs.rmSync(consts.TEXT_DIR, { force: true, recursive: true });
51
- });
52
-
53
- describe("cleanOutputFiles", () => {
54
- it("removes .js, .json, .xml, .strings, .stringsdict files", () => {
55
- cleanOutputDir();
4
+ import { jest } from "@jest/globals";
5
+ import axios from "axios";
6
+ const axiosMock = jest.mocked(axios);
7
+ import fs from "fs";
56
8
 
57
- fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.json"), "test");
58
- fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.js"), "test");
59
- fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.xml"), "test");
60
- fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.strings"), "test");
61
- fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.stringsdict"), "test");
62
- // this file shouldn't be deleted
63
- fs.writeFileSync(path.resolve(consts.TEXT_DIR, "test.txt"), "test");
9
+ jest.mock("fs");
10
+ jest.mock("./api");
64
11
 
65
- expect(fs.readdirSync(consts.TEXT_DIR).length).toEqual(6);
12
+ jest.mock("./http/fetchComponentFolders");
13
+ jest.mock("./http/fetchComponents");
14
+ jest.mock("./http/fetchVariants");
66
15
 
67
- cleanOutputFiles();
16
+ const defaultEnv = { ...process.env };
68
17
 
69
- expect(fs.readdirSync(consts.TEXT_DIR).length).toEqual(1);
70
- });
18
+ beforeEach(() => {
19
+ vol.reset();
20
+ process.env = { ...defaultEnv };
71
21
  });
72
22
 
73
- describe("downloadAndSaveBase", () => {
74
- beforeAll(() => {
75
- if (!fs.existsSync(consts.TEXT_DIR)) {
76
- fs.mkdirSync(consts.TEXT_DIR);
77
- }
78
- });
79
-
80
- beforeEach(() => {
81
- cleanOutputDir();
82
- });
83
-
84
- const mockDataFlat = { hello: "world" };
85
- const mockDataNested = { hello: { text: "world" } };
86
- const mockDataStructured = { hello: { text: "world" } };
87
- const mockDataIcu = { hello: "world" };
88
- const mockDataAndroid = `
89
- <?xml version="1.0" encoding="utf-8"?>
90
- <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
91
- <string name="hello-world" ditto_api_id="hello-world">Hello World</string>
92
- </resources>
93
- `;
94
- const mockDataIosStrings = `
95
- "hello" = "world";
96
- `;
97
- const mockDataIosStringsDict = `
98
- <?xml version="1.0" encoding="utf-8"?>
99
- <plist version="1.0">
100
- <dict>
101
- <key>hello-world</key>
102
- <dict>
103
- <key>NSStringLocalizedFormatKey</key>
104
- <string>%1$#@count@</string>
105
- <key>count</key>
106
- <dict>
107
- <key>NSStringFormatSpecTypeKey</key>
108
- <string>NSStringPluralRuleType</string>
109
- <key>NSStringFormatValueTypeKey</key>
110
- <string>d</string>
111
- <key>other</key>
112
- <string>espanol</string>
113
- </dict>
114
- </dict>
115
- </dict>
116
- </plist>
117
- `;
118
-
119
- const formats: Array<{
120
- format: SupportedFormat;
121
- data: Object;
122
- ext: SupportedExtension;
123
- }> = [
124
- { format: "flat", data: mockDataFlat, ext: ".json" },
125
- { format: "nested", data: mockDataNested, ext: ".json" },
126
- { format: "structured", data: mockDataStructured, ext: ".json" },
127
- { format: "icu", data: mockDataIcu, ext: ".json" },
128
- { format: "android", data: mockDataAndroid, ext: ".xml" },
129
- { format: "ios-strings", data: mockDataIosStrings, ext: ".strings" },
130
- {
131
- format: "ios-stringsdict",
132
- data: mockDataIosStringsDict,
133
- ext: ".stringsdict",
134
- },
135
- ];
136
-
137
- const mockApiCall = (data: unknown) => {
138
- (createApiClient as any).mockImplementation(() => ({
139
- get: () => ({ data }),
140
- }));
141
- };
142
-
143
- const verifySavedData = async (
144
- format: SupportedFormat,
145
- data: unknown,
146
- ext: SupportedExtension
147
- ) => {
148
- const output = await downloadAndSaveBase({
149
- projects: testProjects,
150
- format: format,
151
- } as any);
152
- expect(/successfully saved/i.test(output)).toEqual(true);
153
- const directoryContents = fs.readdirSync(consts.TEXT_DIR);
154
- expect(directoryContents.length).toEqual(testProjects.length);
155
- expect(directoryContents.every((f) => f.endsWith(ext))).toBe(true);
156
- const fileDataString = fs.readFileSync(
157
- path.resolve(consts.TEXT_DIR, directoryContents[0]),
158
- "utf8"
23
+ const mockGlobalConfigFile = `
24
+ api.dittowords.com:
25
+ - token: xxx-xxx-xxx
26
+ `;
27
+ const mockProjectConfigFile = `
28
+ sources:
29
+ components: true
30
+ projects:
31
+ - id: project-id-1
32
+ name: Test Project
33
+ variants: true
34
+ `;
35
+
36
+ describe("pull", () => {
37
+ it("correctly writes files to disk per source for basic config", async () => {
38
+ process.env.DITTO_TEXT_DIR = "/ditto";
39
+ process.env.DITTO_PROJECT_CONFIG_FILE = "/ditto/config.yml";
40
+
41
+ // we need to manually mock responses for the http calls that happen
42
+ // directly within the pull function; we don't need to mock the http
43
+ // calls that happen by way of http/* function calls since those have
44
+ // their own mocks already.
45
+ axiosMock.get.mockImplementation(
46
+ (): Promise<any> => Promise.resolve({ data: "data" })
159
47
  );
160
48
 
161
- switch (format) {
162
- case "android":
163
- case "ios-strings":
164
- case "ios-stringsdict":
165
- expect(typeof data).toBe("string");
166
- expect(fileDataString.replace(/\s/g, "")).toEqual(
167
- (data as string).replace(/\s/g, "")
168
- );
169
- break;
170
-
171
- case "flat":
172
- case "nested":
173
- case "structured":
174
- case "icu":
175
- default:
176
- expect(JSON.parse(fileDataString)).toEqual(data);
177
- break;
178
- }
179
- };
49
+ vol.fromJSON({
50
+ [consts.CONFIG_FILE]: mockGlobalConfigFile,
51
+ [consts.PROJECT_CONFIG_FILE]: mockProjectConfigFile,
52
+ });
180
53
 
181
- formats.forEach(({ format, data, ext }) => {
182
- it(`writes the ${format} format to disk`, async () => {
183
- mockApiCall(data);
184
- await verifySavedData(format, data, ext);
54
+ await pull();
55
+
56
+ const filesOnDiskExpected = new Set([
57
+ "components__example-folder__base.json",
58
+ "components__example-folder__example-variant-1.json",
59
+ "components__example-folder__example-variant-2.json",
60
+ "components__root__base.json",
61
+ "components__root__example-variant-1.json",
62
+ "components__root__example-variant-2.json",
63
+ "test-project__base.json",
64
+ "test-project__example-variant-1.json",
65
+ "test-project__example-variant-2.json",
66
+ "index.d.ts",
67
+ "index.js",
68
+ ]);
69
+
70
+ const filesOnDisk = fs.readdirSync("/ditto");
71
+ filesOnDisk.forEach((file) => {
72
+ filesOnDiskExpected.delete(file);
185
73
  });
186
- });
187
- });
188
74
 
189
- describe("getFormatDataIsValid", () => {
190
- it("handles flat format appropriately", () => {
191
- expect(getFormatDataIsValid.flat("{}")).toBe(false);
192
- expect(getFormatDataIsValid.flat(`{ "hello": "world" }`)).toBe(true);
193
- expect(
194
- getFormatDataIsValid.flat(`{
195
- "__variant-name": "English",
196
- "__variant-description": ""
197
- }`)
198
- ).toBe(false);
199
- expect(
200
- getFormatDataIsValid.flat(`{
201
- "__variant-name": "English",
202
- "__variant-description": "",
203
- "hello": "world"
204
- }`)
205
- ).toBe(true);
206
- });
207
- it("handles structured format appropriately", () => {
208
- expect(getFormatDataIsValid.structured("{}")).toBe(false);
209
- expect(
210
- getFormatDataIsValid.structured(`{ "hello": { "text": "world" } }`)
211
- ).toBe(true);
212
- expect(
213
- getFormatDataIsValid.structured(`{
214
- "__variant-name": "English",
215
- "__variant-description": ""
216
- }`)
217
- ).toBe(false);
218
- expect(
219
- getFormatDataIsValid.structured(`{
220
- "__variant-name": "English",
221
- "__variant-description": "",
222
- "hello": { "text": "world" }
223
- }`)
224
- ).toBe(true);
225
- });
226
- it("handles icu format appropriately", () => {
227
- expect(getFormatDataIsValid.icu("{}")).toBe(false);
228
- expect(getFormatDataIsValid.icu(`{ "hello": "world" }`)).toBe(true);
229
- expect(
230
- getFormatDataIsValid.icu(`{
231
- "__variant-name": "English",
232
- "__variant-description": ""
233
- }`)
234
- ).toBe(false);
235
- expect(
236
- getFormatDataIsValid.icu(`{
237
- "__variant-name": "English",
238
- "__variant-description": "",
239
- "hello": "world"
240
- }`)
241
- ).toBe(true);
242
- });
243
- it("handles android format appropriately", () => {
244
- expect(
245
- getFormatDataIsValid.android(`
246
- <?xml version="1.0" encoding="utf-8"?>
247
- <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>
248
- `)
249
- ).toBe(false);
250
- expect(
251
- getFormatDataIsValid.android(`
252
- <?xml version="1.0" encoding="utf-8"?>
253
- <!--Variant Name: English-->
254
- <!--Variant Description: -->
255
- <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>
256
- `)
257
- ).toBe(false);
258
- expect(
259
- getFormatDataIsValid.android(`
260
- <?xml version="1.0" encoding="utf-8"?>
261
- <!--Variant Name: English-->
262
- <!--Variant Description: -->
263
- <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
264
- <string name="hello-world" ditto_api_id="hello-world">Hello World</string>
265
- </resources>
266
- `)
267
- ).toBe(true);
268
- });
269
- it("handles ios-strings format appropriately", () => {
270
- expect(getFormatDataIsValid["ios-strings"]("")).toBe(false);
271
- expect(
272
- getFormatDataIsValid["ios-strings"](`
273
- /* Variant Name: English */
274
- /* Variant Description: */
275
- `)
276
- ).toBe(false);
277
- expect(
278
- getFormatDataIsValid["ios-strings"](`
279
- /* Variant Name: English */
280
- /* Variant Description: */
281
- "Hello" = "World";
282
- `)
283
- ).toBe(true);
284
- });
285
- it("handles ios-stringsdict format appropriately", () => {
286
- expect(
287
- getFormatDataIsValid["ios-stringsdict"](`
288
- <?xml version="1.0" encoding="utf-8"?>
289
- <plist version="1.0">
290
- <dict/>
291
- </plist>
292
- `)
293
- ).toBe(false);
294
- expect(
295
- getFormatDataIsValid["ios-stringsdict"](`
296
- <?xml version="1.0" encoding="utf-8"?>
297
- <!--Variant Name: English-->
298
- <!--Variant Description: -->
299
- <plist version="1.0">
300
- <dict/>
301
- </plist>
302
- `)
303
- ).toBe(false);
304
- expect(
305
- getFormatDataIsValid["ios-stringsdict"](`
306
- <?xml version="1.0" encoding="utf-8"?>
307
- <!--Variant Name: English-->
308
- <!--Variant Description: -->
309
- <plist version="1.0">
310
- <dict>
311
- <key>Hello World</key>
312
- <dict>
313
- <key>NSStringLocalizedFormatKey</key>
314
- <string>%1$#@count@</string>
315
- <key>count</key>
316
- <dict>
317
- <key>NSStringFormatSpecTypeKey</key>
318
- <string>NSStringPluralRuleType</string>
319
- <key>NSStringFormatValueTypeKey</key>
320
- <string>d</string>
321
- <key>other</key>
322
- <string>espanol</string>
323
- </dict>
324
- </dict>
325
- </dict>
326
- </plist>
327
- `)
328
- ).toBe(true);
75
+ expect(filesOnDiskExpected.size).toBe(0);
329
76
  });
330
77
  });
package/lib/pull.ts CHANGED
@@ -327,7 +327,7 @@ async function downloadAndSave(
327
327
  spinner.start();
328
328
 
329
329
  const [variants, allComponentFoldersResponse] = await Promise.all([
330
- fetchVariants(source),
330
+ fetchVariants(source, options),
331
331
  fetchComponentFolders({}),
332
332
  ]);
333
333
 
@@ -578,15 +578,20 @@ async function downloadAndSave(
578
578
 
579
579
  export interface PullOptions {
580
580
  meta?: Record<string, string>;
581
+ includeSampleData?: boolean;
581
582
  }
582
583
 
583
584
  export const pull = async (options?: PullOptions) => {
584
585
  const meta = options ? options.meta : {};
586
+ const includeSampleData = options?.includeSampleData || false;
585
587
  const token = config.getToken(consts.CONFIG_FILE, consts.API_HOST);
586
588
  const sourceInformation = config.parseSourceInformation();
587
589
 
588
590
  try {
589
- return await downloadAndSave(sourceInformation, token, { meta });
591
+ return await downloadAndSave(sourceInformation, token, {
592
+ meta,
593
+ includeSampleData,
594
+ });
590
595
  } catch (e) {
591
596
  const eventId = Sentry.captureException(e);
592
597
  const eventStr = `\n\nError ID: ${output.info(eventId)}`;
@@ -615,3 +620,9 @@ export default {
615
620
  downloadAndSaveBase,
616
621
  },
617
622
  };
623
+
624
+ export const _test = {
625
+ getJsonFormat,
626
+ getJsonFormatIsValid,
627
+ ensureEndsWithNewLine,
628
+ };
@@ -1,16 +1,30 @@
1
- import fs from "fs/promises";
1
+ import fs from "fs";
2
2
  import { parseOptions, replaceJSXTextInFile } from "./replace"; // Assuming the function is exported in a separate file
3
3
 
4
+ jest.mock("fs");
5
+
4
6
  // Helper function to create a temporary file
5
7
  async function createTempJSXFile(content: string): Promise<string> {
6
- const tempFile = "tempFile.jsx";
7
- await fs.writeFile(tempFile, content);
8
+ const tempFile = "/tempFile.jsx";
9
+ await new Promise((resolve, reject) => {
10
+ try {
11
+ fs.writeFile(tempFile, content, resolve);
12
+ } catch (e) {
13
+ reject(e);
14
+ }
15
+ });
8
16
  return tempFile;
9
17
  }
10
18
 
11
19
  // Helper function to delete the temporary file
12
20
  async function deleteTempFile(filePath: string): Promise<void> {
13
- await fs.unlink(filePath);
21
+ await new Promise((resolve, reject) => {
22
+ try {
23
+ fs.unlink(filePath, resolve);
24
+ } catch (e) {
25
+ reject(e);
26
+ }
27
+ });
14
28
  }
15
29
 
16
30
  describe("parseOptions", () => {
@@ -57,10 +71,9 @@ describe("parseOptions", () => {
57
71
  });
58
72
  });
59
73
 
60
- // TODO: this is flaky
61
74
  describe("replaceJSXTextInFile", () => {
62
75
  afterEach(async () => {
63
- await deleteTempFile("tempFile.jsx");
76
+ await deleteTempFile("/tempFile.jsx");
64
77
  });
65
78
 
66
79
  test("should replace JSX text with a DittoComponent", async () => {
@@ -70,7 +83,15 @@ describe("replaceJSXTextInFile", () => {
70
83
 
71
84
  await replaceJSXTextInFile(tempFile, { searchString, replaceWith }, {});
72
85
 
73
- const transformedCode = await fs.readFile(tempFile, "utf-8");
86
+ const transformedCode = await new Promise((resolve, reject) => {
87
+ fs.readFile(tempFile, "utf-8", (error, data) => {
88
+ if (error) {
89
+ reject(error);
90
+ } else {
91
+ resolve(data);
92
+ }
93
+ });
94
+ });
74
95
  expect(transformedCode).toContain(
75
96
  `<div>Hello, <DittoComponent componentId="${replaceWith}" /></div>`
76
97
  );
@@ -89,7 +110,12 @@ describe("replaceJSXTextInFile", () => {
89
110
  { lineNumbers: [3] }
90
111
  );
91
112
 
92
- const transformedCode = await fs.readFile(tempFile, "utf-8");
113
+ const transformedCode = await new Promise((resolve, reject) =>
114
+ fs.readFile(tempFile, "utf-8", (error, data) => {
115
+ if (error) reject(error);
116
+ else resolve(data);
117
+ })
118
+ );
93
119
  expect(transformedCode).toContain(
94
120
  `<>\n <div>Hello, world</div>\n <div>Hello, <DittoComponent componentId=\"some-id\" /></div>\n</>;`
95
121
  );
@@ -102,7 +128,12 @@ describe("replaceJSXTextInFile", () => {
102
128
 
103
129
  await replaceJSXTextInFile(tempFile, { searchString, replaceWith }, {});
104
130
 
105
- const transformedCode = await fs.readFile(tempFile, "utf-8");
131
+ const transformedCode = await new Promise((resolve, reject) =>
132
+ fs.readFile(tempFile, "utf-8", (error, data) => {
133
+ if (error) reject(error);
134
+ else resolve(data);
135
+ })
136
+ );
106
137
  expect(transformedCode).toContain(
107
138
  `<div>HeLLo, <DittoComponent componentId="${replaceWith}" /></div>`
108
139
  );
@@ -115,7 +146,12 @@ describe("replaceJSXTextInFile", () => {
115
146
 
116
147
  await replaceJSXTextInFile(tempFile, { searchString, replaceWith }, {});
117
148
 
118
- const transformedCode = await fs.readFile(tempFile, "utf-8");
149
+ const transformedCode = await new Promise((resolve, reject) =>
150
+ fs.readFile(tempFile, "utf-8", (error, data) => {
151
+ if (error) reject(error);
152
+ else resolve(data);
153
+ })
154
+ );
119
155
  expect(transformedCode).toContain("<div>Hello, world!</div>");
120
156
  });
121
157
  });
package/lib/replace.ts CHANGED
@@ -1,4 +1,4 @@
1
- import fs from "fs-extra";
1
+ import fs from "fs";
2
2
  import { parse } from "@babel/parser";
3
3
  import traverse from "@babel/traverse";
4
4
  import * as t from "@babel/types";
@@ -11,7 +11,15 @@ async function replaceJSXTextInFile(
11
11
  lineNumbers?: number[];
12
12
  }
13
13
  ) {
14
- const code = await fs.readFile(filePath, "utf-8");
14
+ const code = await new Promise<string>((resolve, reject) =>
15
+ fs.readFile(filePath, "utf-8", (err, data) => {
16
+ if (err) {
17
+ reject(err);
18
+ } else {
19
+ resolve(data);
20
+ }
21
+ })
22
+ );
15
23
  const ast = parse(code, {
16
24
  sourceType: "module",
17
25
  plugins: ["jsx", "typescript"],
@@ -69,7 +77,16 @@ async function replaceJSXTextInFile(
69
77
  /* @ts-ignore */
70
78
  configFile: false,
71
79
  });
72
- fs.writeFile(filePath, transformedCode);
80
+
81
+ await new Promise((resolve, reject) =>
82
+ fs.writeFile(filePath, transformedCode, (err) => {
83
+ if (err) {
84
+ reject(err);
85
+ } else {
86
+ resolve(null);
87
+ }
88
+ })
89
+ );
73
90
  }
74
91
 
75
92
  function splitByCaseInsensitive(str: string, delimiter: string) {
@@ -0,0 +1,48 @@
1
+ import { determineModuleType } from "./determineModuleType";
2
+ import { vol } from "memfs";
3
+
4
+ const defaultEnv = process.env;
5
+
6
+ jest.mock("fs");
7
+
8
+ beforeEach(() => {
9
+ vol.reset();
10
+ process.env = { ...defaultEnv };
11
+ });
12
+
13
+ test("'commonjs' if no package.json found", () => {
14
+ expect(determineModuleType()).toBe("commonjs");
15
+ });
16
+
17
+ test("'commonjs' if package.json found but no `type` property", () => {
18
+ vol.fromJSON({ "package.json": JSON.stringify({}) }, process.cwd());
19
+ expect(determineModuleType()).toBe("commonjs");
20
+ });
21
+
22
+ test("'commonjs' if package.json found and `type` property is 'commonjs'", () => {
23
+ vol.fromJSON({ "package.json": JSON.stringify({ type: "commonjs" }) });
24
+ expect(determineModuleType()).toBe("commonjs");
25
+ });
26
+
27
+ test("'commonjs' if package.json found and `type` property is invalid", () => {
28
+ vol.fromJSON({ "package.json": JSON.stringify({ type: "invalid-type" }) });
29
+ expect(determineModuleType()).toBe("commonjs");
30
+ });
31
+
32
+ test("'module' if package.json found and `type` property is 'module'", () => {
33
+ vol.fromJSON({ "package.json": JSON.stringify({ type: "module" }) });
34
+ expect(determineModuleType()).toBe("module");
35
+ });
36
+
37
+ test("finds package.json in parent directories", () => {
38
+ vol.fromJSON({
39
+ "/some/nested/dir/test.txt": "",
40
+ "/package.json": JSON.stringify({ type: "module" }),
41
+ });
42
+ expect(determineModuleType("/some/nested/dir")).toBe("module");
43
+ });
44
+
45
+ test("supports explicit specification of module type via environment variable", () => {
46
+ process.env.DITTO_MODULE_TYPE = "module";
47
+ expect(determineModuleType()).toBe("module");
48
+ });
@@ -9,18 +9,16 @@ export type ModuleType = "commonjs" | "module";
9
9
  * @returns "commonjs" or "module", defaulting to "module" if no `package.json` is found or if the found
10
10
  * file does not include a `type` property.
11
11
  */
12
- export function determineModuleType() {
13
- const value = getRawTypeFromPackageJson();
12
+ export function determineModuleType(currentDir: string | null = process.cwd()) {
13
+ const value = getRawTypeFromPackageJson(currentDir);
14
14
  return getTypeOrDefault(value);
15
15
  }
16
16
 
17
- function getRawTypeFromPackageJson() {
17
+ function getRawTypeFromPackageJson(currentDir: string | null) {
18
18
  if (process.env.DITTO_MODULE_TYPE) {
19
19
  return process.env.DITTO_MODULE_TYPE;
20
20
  }
21
21
 
22
- let currentDir: string | null = process.cwd(); // Get the current working directory
23
-
24
22
  while (currentDir) {
25
23
  const packageJsonPath = path.join(currentDir, "package.json");
26
24
  if (fs.existsSync(packageJsonPath)) {
@@ -36,7 +34,7 @@ function getRawTypeFromPackageJson() {
36
34
  }
37
35
 
38
36
  if (currentDir === "/") {
39
- return null;
37
+ break;
40
38
  }
41
39
 
42
40
  // Move up a directory and continue the search
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import yaml, { YAMLException } from "js-yaml";
3
3
 
4
- import { PROJECT_CONFIG_FILE } from "../consts";
4
+ import consts from "../consts";
5
5
  import { ConfigYAML, Project } from "../types";
6
6
  import Config, { DEFAULT_CONFIG_JSON } from "../config";
7
7
 
@@ -29,8 +29,8 @@ function yamlToJson(_yaml: string): ConfigYAML | null {
29
29
  * Returns an array containing all valid projects ({ id, name })
30
30
  * currently contained in the project config file.
31
31
  */
32
- export const getSelectedProjects = (configFile = PROJECT_CONFIG_FILE) =>
32
+ export const getSelectedProjects = (configFile = consts.PROJECT_CONFIG_FILE) =>
33
33
  Config.parseSourceInformation(configFile).validProjects;
34
34
 
35
- export const getIsUsingComponents = (configFile = PROJECT_CONFIG_FILE) =>
35
+ export const getIsUsingComponents = (configFile = consts.PROJECT_CONFIG_FILE) =>
36
36
  Config.parseSourceInformation(configFile).shouldFetchComponentLibrary;
package/lib/utils/quit.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export function quit(message: string | null, exitCode = 2) {
2
2
  if (message) console.log(`\n${message}\n`);
3
- process.exitCode = exitCode;
3
+ process.exit(exitCode);
4
4
  }