diff --git a/README.md b/README.md index fcfbae3..21ed88c 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ If a message descriptor has a `description`, it'll be removed from the source af - **`moduleSourceName`**: The ES6 module source name of the React Intl package. Defaults to: `"react-intl"`, but can be changed to another name/path to React Intl. +- **`fields`**: For specifying additional metadata fields and whether they're required. Takes an object with field names as key names, which specify whether they're required. `fields: { metadata: { required: true }, otherdata: {required: false} }` + ### Via CLI ```sh diff --git a/scripts/build-fixtures.js b/scripts/build-fixtures.js index c9c9417..2afd040 100644 --- a/scripts/build-fixtures.js +++ b/scripts/build-fixtures.js @@ -15,6 +15,9 @@ const fixtures = [ ['moduleSourceName', { moduleSourceName: 'react-i18n', }], + ['extractFieldConfig', { + fields: { metadata: { required: true}} + }], ]; fixtures.forEach((fixture) => { diff --git a/src/index.js b/src/index.js index 40b8cde..cebb8e1 100644 --- a/src/index.js +++ b/src/index.js @@ -18,7 +18,11 @@ const FUNCTION_NAMES = [ 'defineMessages', ]; -const DESCRIPTOR_PROPS = new Set(['id', 'description', 'defaultMessage']); +const DESCRIPTOR_PROPS = { + id: {required: true}, + description: {required:false}, + defaultMessage: {required:true}, +}; const EXTRACTED_TAG = Symbol('ReactIntlExtracted'); @@ -82,11 +86,11 @@ export default function ({types: t}) { } } - function createMessageDescriptor(propPaths) { + function createMessageDescriptor(propPaths, fields) { return propPaths.reduce((hash, [keyPath, valuePath]) => { let key = getMessageDescriptorKey(keyPath); - if (DESCRIPTOR_PROPS.has(key)) { + if (Object.keys(fields).indexOf(key) > -1) { hash[key] = valuePath; } @@ -108,12 +112,20 @@ export default function ({types: t}) { return descriptor; } - function storeMessage({id, description, defaultMessage}, path, state) { + function storeMessage(messageDescriptor, path, state) { + const {id, description, defaultMessage} = messageDescriptor; const {file, opts, reactIntl} = state; - if (!(id && defaultMessage)) { + const missing_required = Object.keys(opts.fields) + .filter( (key) => opts.fields[key].required) + .reduce( (arr,key) => { + if(!messageDescriptor[key]){ arr.push( key ); } + return arr; + }, []); + + if(missing_required.length){ throw path.buildCodeFrameError( - '[React Intl] Message Descriptors require an `id` and `defaultMessage`.' + '[React Intl] Message must have the following fields:' + missing_required.join(', ') ); } @@ -130,12 +142,6 @@ export default function ({types: t}) { } } - if (opts.enforceDescriptions && !description) { - throw path.buildCodeFrameError( - '[React Intl] Message must have a `description`.' - ); - } - let loc; if (opts.extractSourceLocation) { loc = { @@ -144,7 +150,7 @@ export default function ({types: t}) { }; } - reactIntl.messages.set(id, {id, description, defaultMessage, ...loc}); + reactIntl.messages.set(id, {...messageDescriptor, ...loc}); } function referencesImport(path, mod, importedNames) { @@ -167,6 +173,10 @@ export default function ({types: t}) { visitor: { Program: { enter(path, state) { + state.opts.fields = state.opts.fields || {}; + const fields = state.opts.fields; + Object.assign(fields,DESCRIPTOR_PROPS); + fields.description.required = !!state.opts.enforceDescriptions; state.reactIntl = { messages: new Map(), }; @@ -228,7 +238,8 @@ export default function ({types: t}) { attributes.map((attr) => [ attr.get('name'), attr.get('value'), - ]) + ]), + opts.fields ); // In order for a default message to be extracted when @@ -290,7 +301,8 @@ export default function ({types: t}) { properties.map((prop) => [ prop.get('key'), prop.get('value'), - ]) + ]), + state.opts.fields ); // Evaluate the Message Descriptor values, then store it. diff --git a/test/fixtures/extractFieldConfig/actual.js b/test/fixtures/extractFieldConfig/actual.js new file mode 100644 index 0000000..2a75bd9 --- /dev/null +++ b/test/fixtures/extractFieldConfig/actual.js @@ -0,0 +1,14 @@ +import React, {Component} from 'react'; +import {FormattedMessage} from 'react-intl'; + +export default class Foo extends Component { + render() { + return ( + + ); + } +} diff --git a/test/fixtures/extractFieldConfig/expected.js b/test/fixtures/extractFieldConfig/expected.js new file mode 100644 index 0000000..ecfda92 --- /dev/null +++ b/test/fixtures/extractFieldConfig/expected.js @@ -0,0 +1,46 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _react = require('react'); + +var _react2 = _interopRequireDefault(_react); + +var _reactIntl = require('react-intl'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var Foo = function (_Component) { + _inherits(Foo, _Component); + + function Foo() { + _classCallCheck(this, Foo); + + return _possibleConstructorReturn(this, (Foo.__proto__ || Object.getPrototypeOf(Foo)).apply(this, arguments)); + } + + _createClass(Foo, [{ + key: 'render', + value: function render() { + return _react2.default.createElement(_reactIntl.FormattedMessage, { + id: 'foo.bar.baz', + metadata: 'metadata content', + defaultMessage: 'Hello World!' + }); + } + }]); + + return Foo; +}(_react.Component); + +exports.default = Foo; diff --git a/test/fixtures/extractFieldConfig/expected.json b/test/fixtures/extractFieldConfig/expected.json new file mode 100644 index 0000000..1ef1e6a --- /dev/null +++ b/test/fixtures/extractFieldConfig/expected.json @@ -0,0 +1,7 @@ +[ + { + "id": "foo.bar.baz", + "metadata": "metadata content", + "defaultMessage": "Hello World!" + } +] diff --git a/test/index.js b/test/index.js index 41ddd8b..33c3bf2 100644 --- a/test/index.js +++ b/test/index.js @@ -16,6 +16,7 @@ const skipTests = [ 'moduleSourceName', 'icuSyntax', 'removeDescriptions', + 'extractFieldConfig', ]; const fixturesDir = path.join(__dirname, 'fixtures'); @@ -39,9 +40,9 @@ describe('emit asserts for: ', () => { assert.equal(trim(actual), trim(expected)); // Check message output - const expectedMessages = fs.readFileSync(path.join(fixtureDir, 'expected.json')); - const actualMessages = fs.readFileSync(path.join(fixtureDir, 'actual.json')); - assert.equal(trim(actualMessages), trim(expectedMessages)); + const expectedMessages = require(path.join(fixtureDir, 'expected.json')); + const actualMessages = require(path.join(fixtureDir, 'actual.json')); + assert.deepEqual(actualMessages, expectedMessages); }); }); }); @@ -57,7 +58,8 @@ describe('options', () => { assert(false); } catch (e) { assert(e); - assert(/Message must have a `description`/.test(e.message)); + assert(/Message must have the following fields/.test(e.message)); + assert(/description/.test(e.message)); } }); @@ -105,9 +107,9 @@ describe('options', () => { } // Check message output - const expectedMessages = fs.readFileSync(path.join(fixtureDir, 'expected.json')); - const actualMessages = fs.readFileSync(path.join(fixtureDir, 'actual.json')); - assert.equal(trim(actualMessages), trim(expectedMessages)); + const expectedMessages = require(path.join(fixtureDir, 'expected.json')); + const actualMessages = require(path.join(fixtureDir, 'actual.json')); + assert.deepEqual(actualMessages, expectedMessages); }); it('respects extractSourceLocation', () => { @@ -124,9 +126,43 @@ describe('options', () => { } // Check message output - const expectedMessages = fs.readFileSync(path.join(fixtureDir, 'expected.json')); - const actualMessages = fs.readFileSync(path.join(fixtureDir, 'actual.json')); - assert.equal(trim(actualMessages), trim(expectedMessages)); + const expectedMessages = require(path.join(fixtureDir, 'expected.json')); + const actualMessages = require(path.join(fixtureDir, 'actual.json')); + assert.deepEqual(actualMessages, expectedMessages); + }); + + it('respects field config, extracts data', () => { + const fixtureDir = path.join(fixturesDir, 'extractFieldConfig'); + + try { + transform(path.join(fixtureDir, 'actual.js'), { + fields: { metadata: { required: true}}, + }); + assert(true); + } catch (e) { + console.error(e); + assert(false); + } + + // Check message output + const expectedMessages = require(path.join(fixtureDir, 'expected.json')); + const actualMessages = require(path.join(fixtureDir, 'actual.json')); + assert.deepEqual(actualMessages, expectedMessages); + }); + + it('enforces field config required setting', () => { + const fixtureDir = path.join(fixturesDir, 'extractSourceLocation'); + + try { + transform(path.join(fixtureDir, 'actual.js'), { + fields: { metadata: { required: true}}, + }); + assert(false); + } catch (e) { + assert(e); + assert(/Message must have the following fields/.test(e.message)); + assert(/metadata/.test(e.message)); + } }); });