diff --git a/package.json b/package.json index 5d413ca..e70ad26 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,10 @@ "tasks" ], "dependencies": { - "acorn-jsx": "^4.1.1", - "acorn-jsx-walk": "^1.0.1", + "acorn": "^6.0.4", + "acorn-jsx": "^5.0.1", + "acorn-stage3": "^1.0.0", + "acorn-walk": "^6.1.1", "chalk": "^2.4.1", "clone-deep": "^4.0.0", "commander": "^2.15.1", diff --git a/src/acorn-jsx-walk.js b/src/acorn-jsx-walk.js new file mode 100644 index 0000000..dec904c --- /dev/null +++ b/src/acorn-jsx-walk.js @@ -0,0 +1,62 @@ +// Originally from: https://github.com/sderosiaux/acorn-jsx-walk + +import { Parser } from 'acorn'; +import { simple as walk, base } from 'acorn-walk'; +import jsx from 'acorn-jsx'; +import stage3 from 'acorn-stage3'; + +// +// Extends acorn walk with JSX elements +// + +// See: https://github.com/RReverser/acorn-jsx/issues/23#issuecomment-403753801 +Object.assign(base, { + FieldDefinition(node, state, callback) { + if (node.value !== null) { + callback(node.value, state); + } + }, + + JSXAttribute(node, state, callback) { + if (node.value !== null) { + callback(node.value, state); + } + }, + + JSXElement(node, state, callback) { + node.openingElement.attributes.forEach(attribute => { + callback(attribute, state); + }); + node.children.forEach(node => { + callback(node, state); + }); + }, + + JSXEmptyExpression(node, state, callback) { + // Comments. Just ignore. + }, + + JSXExpressionContainer(node, state, callback) { + callback(node.expression, state); + }, + + JSXFragment(node, state, callback) { + node.children.forEach(node => { + callback(node, state); + }); + }, + + JSXSpreadAttribute(node, state, callback) { + callback(node.argument, state); + }, + + JSXText() {} +}); + +export default (source, options) => { + const ast = Parser.extend(stage3, jsx()).parse(source, { + sourceType: 'module', + ecmaVersion: 10, + }); + walk(ast, options || {}); +}; diff --git a/src/parser.js b/src/parser.js index 4230c4d..ad0b644 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1,7 +1,6 @@ /* eslint no-console: 0 */ /* eslint no-eval: 0 */ import fs from 'fs'; -import jsxwalk from 'acorn-jsx-walk'; import chalk from 'chalk'; import cloneDeep from 'clone-deep'; import deepMerge from 'deepmerge'; @@ -10,6 +9,7 @@ import { parse } from 'esprima'; import _ from 'lodash'; import parse5 from 'parse5'; import sortObject from 'sortobject'; +import jsxwalk from './acorn-jsx-walk'; import flattenObjectKeys from './flatten-object-keys'; import omitEmptyObject from './omit-empty-object'; import nodesToString from './nodes-to-string'; diff --git a/test/fixtures/modules/index.js b/test/fixtures/modules/index.js index 08011ec..f47e038 100644 --- a/test/fixtures/modules/index.js +++ b/test/fixtures/modules/index.js @@ -10,7 +10,7 @@ var msg = [ _t('YouTube has more than {{count}} billion users.', {count: 1}), _t('You have {{count}} messages.', { count: 10 - }); + }), ].join('\n'); console.log(msg); diff --git a/test/fixtures/trans-acorn-broken.jsx b/test/fixtures/trans-acorn-broken.jsx new file mode 100644 index 0000000..5a336e0 --- /dev/null +++ b/test/fixtures/trans-acorn-broken.jsx @@ -0,0 +1,11 @@ +import { Fragment } from 'react'; + +// Provoke an acorn parsing error by inserting a `async` keyword in a wrong place. This should lead +// to no translateions being extracted from this file. +const async Component = () => ( + + Broken + +); + +export default Component; diff --git a/test/fixtures/trans-acorn.jsx b/test/fixtures/trans-acorn.jsx new file mode 100644 index 0000000..48e9310 --- /dev/null +++ b/test/fixtures/trans-acorn.jsx @@ -0,0 +1,37 @@ +import { Fragment } from 'react'; + +class Component extends React.Component { + // noop just to see if acorn can parse this + state = { + }; + + static async onClick( + a, + b, + ...args + ) { + console.log(a, b, ...args); + // noop just to see if acorn can parse this + } + + render() { + // This does not work yet. + const spreadProps = { + i18nKey: 'spread', + }; + + return ( + + { + // Empty expression should not fail + } + <> + Simple i18nKey + Spread i18nKey + + + ); + } +} + +export default Component; diff --git a/test/fixtures/trans.jsx b/test/fixtures/trans.jsx index 0859944..6a9d68d 100644 --- a/test/fixtures/trans.jsx +++ b/test/fixtures/trans.jsx @@ -2,6 +2,9 @@ import { Fragment } from 'react'; const Component = () => ( + { + // Empty expression should not fail + } Use double quotes for the i18nKey attribute Use single quote for the i18nKey attribute diff --git a/test/jsx-parser.js b/test/jsx-parser.js index ac76d78..f53e3ad 100644 --- a/test/jsx-parser.js +++ b/test/jsx-parser.js @@ -1,14 +1,13 @@ import { test } from 'tap'; -import { parse } from 'acorn-jsx'; +import { Parser } from 'acorn'; +import jsx from 'acorn-jsx'; import ensureArray from 'ensure-array'; import _get from 'lodash/get'; import nodesToString from '../src/nodes-to-string'; const jsxToString = (code) => { try { - const ast = parse(`${code}`, { - plugins: { jsx: true } - }); + const ast = Parser.extend(jsx()).parse(`${code}`); const nodes = ensureArray(_get(ast, 'body[0].expression.children')); if (nodes.length === 0) { diff --git a/test/parser.js b/test/parser.js index 37e1b6f..e8ec5fc 100644 --- a/test/parser.js +++ b/test/parser.js @@ -254,6 +254,53 @@ test('Parse wrapped Trans components', (t) => { t.end(); }); +test('Parse Trans components with modern acorn features', (t) => { + const parser = new Parser({ + lngs: ['en'], + trans: { + fallbackKey: true + }, + nsSeparator: false, + keySeparator: '.', // Specify the keySeparator for this test to make sure the fallbackKey won't be separated + fallbackLng: 'en' + }); + + const content = fs.readFileSync(path.resolve(__dirname, 'fixtures/trans-acorn.jsx'), 'utf-8'); + parser.parseTransFromString(content); + t.same(parser.get(), { + en: { + translation: { + // Passing keys to via object spread is not yet supported: + 'Spread i18nKey': 'Spread i18nKey', + // 'spread': 'Spread i18nKey', // this would be expected. + 'simple': 'Simple i18nKey' + } + } + }); + t.end(); +}); + +test('Parse Trans components should fail with broken syntax', (t) => { + const parser = new Parser({ + lngs: ['en'], + trans: { + fallbackKey: true + }, + nsSeparator: false, + keySeparator: '.', // Specify the keySeparator for this test to make sure the fallbackKey won't be separated + fallbackLng: 'en' + }); + + const content = fs.readFileSync(path.resolve(__dirname, 'fixtures/trans-acorn-broken.jsx'), 'utf-8'); + parser.parseTransFromString(content); + t.same(parser.get(), { + en: { + translation: {} + } + }); + t.end(); +}); + test('Parse HTML attribute', (t) => { test('parseAttrFromString(content)', (t) => { const parser = new Parser({