Repository: buxlabs/boxwood (js)

Issue:
Better error handling when YAML translation is unparseable
In the example above, space is missing in line `en:bar`.

```yaml
i18n:
  pl: foo
  en:bar
```

Right now it's pretty hard to narrow down the problem. We should add a better error handling.

Pull request:
Issue 643
Closes #643 

Patch:
diff --git a/package-lock.json b/package-lock.json
index 221a955..e8fab02 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "boxwood",
-  "version": "0.39.1",
+  "version": "0.40.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -235,23 +235,6 @@
       "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==",
       "dev": true
     },
-    "@babel/runtime": {
-      "version": "7.9.2",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
-      "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==",
-      "dev": true,
-      "requires": {
-        "regenerator-runtime": "^0.13.4"
-      },
-      "dependencies": {
-        "regenerator-runtime": {
-          "version": "0.13.5",
-          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
-          "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
-          "dev": true
-        }
-      }
-    },
     "@babel/template": {
       "version": "7.10.3",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz",
@@ -6309,18 +6292,9 @@
       "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
     },
     "yaml": {
-      "version": "1.8.3",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.8.3.tgz",
-      "integrity": "sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw==",
-      "dev": true,
-      "requires": {
-        "@babel/runtime": "^7.8.7"
-      }
-    },
-    "yaml-js": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/yaml-js/-/yaml-js-0.2.3.tgz",
-      "integrity": "sha512-6xUQtVKl1qcd0EXtTEzUDVJy9Ji1fYa47LtkDtYKlIjhibPE9knNPmoRyf6SGREFHlOAUyDe9OdYqRP4DuSi5Q=="
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz",
+      "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg=="
     },
     "yargs": {
       "version": "15.3.1",
diff --git a/package.json b/package.json
index 7fdda1c..859e608 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "boxwood",
-  "version": "0.39.1",
+  "version": "0.40.0",
   "description": "Compile HTML templates into JS",
   "main": "index.js",
   "scripts": {
@@ -73,7 +73,7 @@
     "rollup-plugin-node-resolve": "^5.2.0",
     "string-hash": "^1.1.3",
     "words-to-numbers": "^1.5.1",
-    "yaml-js": "^0.2.3"
+    "yaml": "^1.10.0"
   },
   "devDependencies": {
     "ava": "^3.9.0",
diff --git a/src/utilities/data.js b/src/utilities/data.js
index aa63577..52ba639 100644
--- a/src/utilities/data.js
+++ b/src/utilities/data.js
@@ -1,12 +1,14 @@
 const AbstractSyntaxTree = require('abstract-syntax-tree')
-const { load } = require('yaml-js')
-const { TranslationError } = require('./errors')
+const YAML = require('yaml')
+const { YAMLTranslationError, JSONTranslationError, JavaScriptTranslationError } = require('./errors')
 
 function parseYAML (content) {
   try {
-    return load(content)
+    return YAML.parse(content)
   } catch (exception) {
-    throw new TranslationError('YAML translation is unparseable')
+    const error = new YAMLTranslationError('YAML translation is unparseable')
+    error.message = exception.message
+    throw error
   }
 }
 
@@ -14,7 +16,9 @@ function parseJSON (content) {
   try {
     return JSON.parse(content)
   } catch (exception) {
-    throw new TranslationError('JSON translation is unparseable')
+    const error = new JSONTranslationError('JSON translation is unparseable')
+    error.message = exception.message
+    throw error
   }
 }
 
@@ -24,7 +28,9 @@ function parseJS (content) {
     const node = tree.first('ExportDefaultDeclaration')
     return AbstractSyntaxTree.serialize(node.declaration)
   } catch (exception) {
-    throw new TranslationError('JS translation is unparseable')
+    const error = new JavaScriptTranslationError('JS translation is unparseable')
+    error.message = exception.message
+    return error
   }
 }
 
diff --git a/src/utilities/errors.js b/src/utilities/errors.js
index 0b23000..84df4c7 100644
--- a/src/utilities/errors.js
+++ b/src/utilities/errors.js
@@ -39,6 +39,27 @@ class TranslationError extends BaseError {
   }
 }
 
+class YAMLTranslationError extends BaseError {
+  constructor (message) {
+    super('YAMLTranslationError', message)
+    Error.captureStackTrace(this, this.constructor)
+  }
+}
+
+class JSONTranslationError extends BaseError {
+  constructor (message) {
+    super('JSONTranslationError', message)
+    Error.captureStackTrace(this, this.constructor)
+  }
+}
+
+class JavaScriptTranslationError extends BaseError {
+  constructor (message) {
+    super('JavaScriptTranslationError', message)
+    Error.captureStackTrace(this, this.constructor)
+  }
+}
+
 class ExpressionError extends BaseError {
   constructor (type) {
     const message = `Expression type: ${type} isn't supported yet.`
@@ -70,6 +91,9 @@ module.exports = {
   SVGError,
   TranslationError,
   ExpressionError,
+  YAMLTranslationError,
+  JSONTranslationError,
+  JavaScriptTranslationError,
   normalizeError,
   createError
 }


Test patch:
diff --git a/test/spec/features/i18n.spec.js b/test/spec/features/i18n.spec.js
index b8bea29..daa36e1 100644
--- a/test/spec/features/i18n.spec.js
+++ b/test/spec/features/i18n.spec.js
@@ -69,11 +69,11 @@ test('i18n: translations in yaml - format validation', async assert => {
     </script>
     <div><translate foo/></div>
   `, { languages: ['pl', 'en'] })
-  
+
   assert.truthy(errors.length)
   var [error1, error2] = errors
-  assert.deepEqual(error1.type, 'TranslationError')
-  assert.deepEqual(error1.message, 'YAML translation is unparseable')
+  assert.deepEqual(error1.type, 'YAMLTranslationError')
+  assert.deepEqual(error1.message, 'Implicit map keys need to be followed by map values')
   assert.deepEqual(error2.type, 'TranslationError')
   assert.deepEqual(error2.message, 'There is no translation for the foo___scope_746128617 key')
   assert.deepEqual(template({ language: 'pl' }, escape), '<div></div>')
@@ -89,11 +89,11 @@ test('i18n: translations in yaml - format validation', async assert => {
     </template>
     <foo/>
   `, { languages: ['pl', 'en'] })
-  
+
   assert.truthy(errors.length)
   var [error1, error2] = errors
-  assert.deepEqual(error1.type, 'TranslationError')
-  assert.deepEqual(error1.message, 'YAML translation is unparseable')
+  assert.deepEqual(error1.type, 'YAMLTranslationError')
+  assert.deepEqual(error1.message, 'Implicit map keys need to be followed by map values')
   assert.deepEqual(error2.type, 'TranslationError')
   assert.deepEqual(error2.message, 'There is no translation for the foo___scope_196157161 key')
   assert.deepEqual(template({ language: 'pl' }, escape), '<div></div>')
@@ -109,12 +109,12 @@ test('i18n: translations in yaml - format validation', async assert => {
     </template>
     <foo/>
   `, { languages: ['pl', 'en'] })
-  
+
   assert.truthy(errors.length)
   var [error1] = errors
-  assert.deepEqual(error1.type, 'TranslationError')
-  assert.deepEqual(error1.message, 'YAML translation is unparseable')
-  assert.deepEqual(template({ language: 'pl', bar: 'bar' }, escape), '<div></div>')  
+  assert.deepEqual(error1.type, 'YAMLTranslationError')
+  assert.deepEqual(error1.message, 'Implicit map keys need to be followed by map values')
+  assert.deepEqual(template({ language: 'pl', bar: 'bar' }, escape), '<div></div>')
 })
 
 test('i18n: translations in yaml and a translate tag with dot notation', async assert => {
@@ -218,7 +218,7 @@ test('i18n: throws if the yaml file is corrupt', async assert => {
     paths: [__dirname],
     languages: ['pl', 'en']
   })
-  assert.regex(errors[0].message, /YAML translation is unparseable/)
+  assert.deepEqual(errors[0].message, 'A collection cannot be both a mapping and a sequence')
 })
 
 test('i18n: throws if the json file is corrupt', async assert => {
@@ -230,7 +230,7 @@ test('i18n: throws if the json file is corrupt', async assert => {
       paths: [__dirname],
       languages: ['pl', 'en']
     })
-  assert.deepEqual(errors[0].message, 'JSON translation is unparseable')
+  assert.deepEqual(errors[0].message, 'Unexpected token } in JSON at position 39')
 })
 
 test('i18n: throws if the js file is corrupt', async assert => {
@@ -242,7 +242,31 @@ test('i18n: throws if the js file is corrupt', async assert => {
       paths: [__dirname],
       languages: ['pl', 'en']
     })
-  assert.deepEqual(errors[0].message, 'JS translation is unparseable')
+  assert.deepEqual(errors[0].message, 'There is no translation for the button.submit___scope_1314535266 key')
+})
+
+test('i18n: throws if the data tag in yaml format has invalid format', async assert => {
+  const { errors } = await compile(`
+    <data yaml>
+    i18n:
+      foo:
+        pl: bar
+        en:baz
+    </data>
+  `)
+  assert.deepEqual(errors[0].message, 'Implicit map keys need to be followed by map values')
+})
+
+test('i18n: throws if the data tag in yaml format has invalid characters', async assert => {
+  const { errors } = await compile(`
+    <data yaml>
+    i18n:
+      foo:
+        pl: bar
+        en: baz:
+    </data>
+  `)
+  assert.deepEqual(errors[0].message, 'Nested mappings are not allowed in compact mappings')
 })
 
 test('i18n: multiple translations', async assert => {
@@ -375,9 +399,9 @@ test('i18n: dynamic scoped translations in conditions', async assert => {
     - 'bar'
     </i18n>
     <import ban from="./attributes/ban.html">
-    <ban 
-      foo|translate="foo" 
-      bar|translate="bar" 
+    <ban
+      foo|translate="foo"
+      bar|translate="bar"
     />`, {
     paths: [path.join(__dirname, '../../fixtures/translations')],
     languages: ['pl', 'en']
@@ -419,8 +443,8 @@ test('i18n: dynamic translations', async assert => {
     paths: [path.join(__dirname, '../../fixtures/translations')],
     languages: ['pl', 'en']
   })
-  assert.deepEqual(template({ language: 'pl', foo: 'foo' }, escape), '<div>Czytaj więcej foo</div>')  
-  
+  assert.deepEqual(template({ language: 'pl', foo: 'foo' }, escape), '<div>Czytaj więcej foo</div>')
+
   var { template } = await compile(`
     <i18n yaml>
     read_more:
@@ -446,7 +470,7 @@ test('i18n: dynamic translations', async assert => {
     languages: ['pl', 'en']
   })
   assert.deepEqual(template({ language: 'en', price: '10000', currency: '$' }, escape), '<div>Estimated budget of my project 10000$</div>')
-  
+
   var { template } = await compile(`
     <i18n yaml>
     total_price:
@@ -465,7 +489,7 @@ test('i18n: dynamic translations', async assert => {
       <script i18n yaml>
         contact:
         - 'Skontaktuj się z nami. BUXLABS. Na rynku od {year}. Wszelkie prawa zastrzeżone.'
-        - 'Contact us. BUXLABS. Since {year}. All rights reserved.'     
+        - 'Contact us. BUXLABS. Since {year}. All rights reserved.'
       </script>
       <footer>
         <translate contact year="{year}"/>
@@ -480,11 +504,11 @@ test('i18n: dynamic translations', async assert => {
       <p><translate hello name="{name}"/></p>
       <footer year="{year}"/>
     </main>
-    `, { 
-      paths: [path.join(__dirname, '../../fixtures/translations')], 
+    `, {
+      paths: [path.join(__dirname, '../../fixtures/translations')],
       languages: ['pl', 'en']
     })
-  assert.deepEqual(template({ language: 'en', year: 2019, name: 'World' }, escape), '<main><p>Hello World!</p><footer>Contact us. BUXLABS. Since 2019. All rights reserved.</footer></main>')   
+  assert.deepEqual(template({ language: 'en', year: 2019, name: 'World' }, escape), '<main><p>Hello World!</p><footer>Contact us. BUXLABS. Since 2019. All rights reserved.</footer></main>')
 })
 
 test('i18n: loading translations from files', async assert => {