Repository: gilbsgilbs/babel-plugin-i18next-extract (ts)

Issue:
Plugin crashes when using returnObjects option
I use the plugin in a nextjs project. When i build sometimes it fails with a compilation error like so
`Failed to compile.

./pages/index.js
Error: /Users/mozenge/Desktop/work/movinga/explorations/helloNnext/pages/index.js:

> Build error occurred
Error: > Build failed because of webpack errors
    at build (/Users/mozenge/Desktop/explorations/helloNext/node_modules/next/dist/build/index.js:9:900)

After commenting and uncommenting lines of code I was able to pinpoint failure points, i.e places in the code that caused the compilation errors.
I created a sample project with config to test it out easily here: [sample repo](https://github.com/oaks-view/next-i18next-scanner-test).

In the `pages/index.js` build / compilation breaks at line 6 and line 11.
Line 6:
`const marks = Object.values(t('journey.step.area.marks', { returnObjects: true, defaultValue: {} })).map(mark => ({
    ...mark,
    value: Number.parseInt(mark.value, 10),
  }));
`
Line 11:
`const min = Number.parseInt(t('journey.step.years-living.min', { defaultValue: 2 }), 10);
`
Running the command `npm run build`  in the [sample repo](https://github.com/oaks-view/next-i18next-scanner-test). can show this behavior.
So when the lines 6 and 11 are commented out Build doesnt fail.

I couldnt find in the docs where codes like the above are flagged as edge cases.

- Plugin version : 0.4.2
- Node version: 10.16.0


Pull request:
proposed fix for issue 117
Fixes #117 

Patch:
diff --git a/src/exporters/jsonv3.ts b/src/exporters/jsonv3.ts
index 5ea4a1a..7cd8260 100644
--- a/src/exporters/jsonv3.ts
+++ b/src/exporters/jsonv3.ts
@@ -3,16 +3,16 @@ import stringify from 'json-stable-stringify';
 import { Exporter, ConflictError } from './commons';
 
 /**
- * A deep, recursive object which leaves must be of generic type V.
- */
-interface DeepObject<V = string> {
-  [k: string]: DeepObject<V> | V;
-}
-
-/**
- * JSON keys can either be strings or null.
+ * JSONv3 values can be any valid value for JSON file.
+ *
+ * See i18next's "returnObjects" option.
  */
-type JsonV3Key = string | null;
+type JsonV3Value =
+  | string
+  | number
+  | null
+  | JsonV3Value[]
+  | { [k: string]: JsonV3Value };
 
 /**
  * Content of a JSON v3 file.
@@ -20,7 +20,16 @@ type JsonV3Key = string | null;
 interface JsonV3File {
   whitespacesBefore: string;
   whitespacesAfter: string;
-  content: DeepObject<JsonV3Key>;
+  content: { [k: string]: JsonV3Value };
+}
+
+/**
+ * Check whether a JsonV3Value is a plain object.
+ */
+function jsonV3ValueIsObject(
+  val: JsonV3Value,
+): val is { [k: string]: JsonV3Value } {
+  return typeof val === 'object' && val !== null && !Array.isArray(val);
 }
 
 /**
@@ -35,7 +44,7 @@ function recursiveAddKey(
   fileContent: JsonV3File['content'],
   keyPath: string[],
   cleanKey: string,
-  value: string | null,
+  value: JsonV3Value,
 ): JsonV3File['content'] {
   if (keyPath.length === 0) {
     return {
@@ -48,7 +57,7 @@ function recursiveAddKey(
   let current = fileContent[currentKeyPath];
   if (current === undefined) {
     current = {};
-  } else if (current === null || typeof current !== 'object') {
+  } else if (!jsonV3ValueIsObject(current)) {
     throw new ConflictError();
   }
 
@@ -63,7 +72,7 @@ function recursiveAddKey(
   };
 }
 
-const jsonv3Exporter: Exporter<JsonV3File, JsonV3Key> = {
+const jsonv3Exporter: Exporter<JsonV3File, JsonV3Value> = {
   init: () => {
     return {
       whitespacesBefore: '',
@@ -95,17 +104,12 @@ const jsonv3Exporter: Exporter<JsonV3File, JsonV3Key> = {
       const val = current[p];
       if (val === undefined) {
         return undefined;
-      }
-      if (typeof val === 'string' || val === null) {
+      } else if (!jsonV3ValueIsObject(val)) {
         throw new ConflictError();
       }
       current = val;
     }
-    const val = current[cleanKey];
-    if (typeof val !== 'string' && val !== null && val !== undefined) {
-      throw new ConflictError();
-    }
-    return val;
+    return current[cleanKey];
   },
   addKey: params => {
     const { key, file, value } = params;


Test patch:
diff --git a/tests/__fixtures__/testTFunction/uncommonValuesTypes.js b/tests/__fixtures__/testTFunction/uncommonValuesTypes.js
new file mode 100644
index 0000000..fabc009
--- /dev/null
+++ b/tests/__fixtures__/testTFunction/uncommonValuesTypes.js
@@ -0,0 +1,31 @@
+t(
+  'object',
+  {
+    returnObjects: true,
+    defaultValue: {
+      hello: 'world',
+      value: 42,
+      itsnull: null,
+      nested: {
+        obj: {},
+        arr: [],
+      },
+    },
+  },
+);
+
+t(
+  'array',
+  {
+    returnObjects: true,
+    defaultValue: ['pasta', {hello: 'world'}, 42, null, ['nested']],
+  },
+);
+
+t(
+  'number',
+  {
+    returnObjects: true,
+    defaultValue: 42,
+  },
+);
\ No newline at end of file
diff --git a/tests/__fixtures__/testTFunction/uncommonValuesTypes.json b/tests/__fixtures__/testTFunction/uncommonValuesTypes.json
new file mode 100644
index 0000000..4b320c5
--- /dev/null
+++ b/tests/__fixtures__/testTFunction/uncommonValuesTypes.json
@@ -0,0 +1,18 @@
+{
+  "description": "test extraction of values with uncommon types",
+  "comment": "see https://github.com/gilbsgilbs/babel-plugin-i18next-extract/issues/117",
+  "pluginOptions": {},
+  "expectValues": {
+    "object": {
+      "hello": "world",
+      "value": 42,
+      "itsnull": null,
+      "nested": {
+        "obj": {},
+        "arr": []
+      }
+    },
+    "array": ["pasta", {"hello": "world"}, 42, null, ["nested"]],
+    "number": 42
+  }
+}
diff --git a/tests/exporters/exporter.test.ts b/tests/exporters/exporter.test.ts
index 4a713a0..1a1002e 100644
--- a/tests/exporters/exporter.test.ts
+++ b/tests/exporters/exporter.test.ts
@@ -62,33 +62,6 @@ describe('Test exporter works', () => {
     expect(fs.readJSONSync(outputPath)).toEqual({ deep: 'has value' });
   });
 
-  it('throws an ExportError when the value is already an object', () => {
-    const outputPath = path.join(
-      outputDir,
-      'throws_if_already_an_object.json',
-    );
-    fs.writeJSONSync(outputPath, { alreadyDeep: { someKey: 'foo' } });
-
-    const config = parseConfig({ outputPath });
-    const key = createSimpleKey('alreadyDeep');
-
-    let hasThrown = false;
-    try {
-      exportTranslationKeys([key], 'fr', config, createExporterCache());
-    } catch (err) {
-      if (!(err instanceof ExportError)) throw err;
-      hasThrown = true;
-    }
-
-    expect(
-      hasThrown,
-      'Expected ExportError, but no exception was thrown',
-    ).toBe(true);
-    expect(fs.readJSONSync(outputPath)).toEqual({
-      alreadyDeep: { someKey: 'foo' },
-    });
-  });
-
   it('can discard old keys', () => {
     const outputPath = path.join(outputDir, 'discard_old_keys.json');
     fs.writeJSONSync(outputPath, { oldKey: 'foo' });
diff --git a/tests/exporters/jsonv3.test.ts b/tests/exporters/jsonv3.test.ts
index 2f9f297..98abfb8 100644
--- a/tests/exporters/jsonv3.test.ts
+++ b/tests/exporters/jsonv3.test.ts
@@ -99,20 +99,35 @@ describe('Test JSONv3 exporter', () => {
     ).toBeUndefined();
   });
 
-  it('cannot get conflicting key', () => {
+  it('throws ConflictError when path contains non JSON object', () => {
     expect(() =>
       jsonv3Exporter.getKey({
         config,
-        keyPath: [],
-        cleanKey: 'hello',
+        keyPath: ['hello'],
+        cleanKey: 'world',
         file: {
           whitespacesBefore: '',
           whitespacesAfter: '',
-          content: { hello: { new: 'world' } },
+          content: { hello: 250 },
+        },
+      }),
+    ).toThrow(ConflictError);
+
+    expect(() =>
+      jsonv3Exporter.getKey({
+        config,
+        keyPath: ['hello'],
+        cleanKey: 'world',
+        file: {
+          whitespacesBefore: '',
+          whitespacesAfter: '',
+          content: { hello: [] },
         },
       }),
     ).toThrow(ConflictError);
+  });
 
+  it('cannot get conflicting key', () => {
     expect(() =>
       jsonv3Exporter.getKey({
         config,