diff --git a/.babelrc b/.babelrc index 69803ca..69ade47 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,6 @@ { "presets": [ - "react", - "es2015" + "@babel/react", + "@babel/env" ] } diff --git a/.gitignore b/.gitignore index d9618b1..d30cf32 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules .DS_Store lib/ .idea/ +.build/ diff --git a/.npmignore b/.npmignore index 7b81124..45d50b3 100644 --- a/.npmignore +++ b/.npmignore @@ -2,3 +2,4 @@ src/ /npm-debug.log* .DS_Store .idea/ +.build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0245459..22bad84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2.0.0 +* Breaking change: require React ^16.2.0 (for native Fragment support) +* Breaking change: `react` and `react-com` are now `peerDependencies`. +* Drop deprecated react-addons-create-fragment from library +* Update to Babel 7 +* Add Terser to build pipeline for minification + ## 1.1.1 * Drop deprecated React.createClass, React.DOM from test * Bump to allow for React ^16.0.0 diff --git a/circle.yml b/circle.yml index 6161605..0faacf9 100644 --- a/circle.yml +++ b/circle.yml @@ -1,3 +1,10 @@ -machine: - node: - version: 5.11.1 +version: 2.1 + +jobs: + build: + docker: + - image: node:lts-slim + steps: + - checkout + - npm install + - npm run test diff --git a/package.json b/package.json index 00253f3..77e79f5 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,43 @@ { "name": "interpolate-components", - "version": "1.1.1", + "version": "2.0.0", "description": "Convert strings into structured React components.", "repository": { "type": "git", "url": "git+https://github.com/Automattic/interpolate-components.git" }, - "main": "lib/index.js", + "main": "lib/index.min.js", "keywords": [ "react", "react-component", "interpolation" ], "scripts": { - "compile": "babel -sd lib/ src/", - "prepublish": "npm run compile", - "test": "mocha --require babel-register --reporter spec test/test.jsx" + "clean": "rimraf .build lib", + "compile": "babel --source-maps=inline -d .build/ src/", + "minify:index": "terser .build/index.js --compress --mangle --source-map 'content=inline' -o lib/index.min.js", + "minify:tokenize": "terser .build/tokenize.js --compress --mangle --source-map 'content=inline' -o lib/tokenize.min.js", + "minify": "mkdirp lib && npm run minify:index && npm run minify:tokenize", + "build": "npm run clean && npm run compile && npm run minify", + "prepare": "npm run build", + "test": "mocha --require @babel/register --reporter spec test/test.jsx" }, "devDependencies": { - "babel-cli": "^6.9.0", - "babel-core": "^6.9.1", - "babel-preset-es2015": "^6.9.0", - "babel-preset-react": "^6.5.0", - "babel-register": "^6.9.0", - "mocha": "^2.3.4" + "@babel/cli": "^7.5.5", + "@babel/core": "^7.5.5", + "@babel/preset-env": "^7.5.5", + "@babel/preset-react": "^7.0.0", + "@babel/register": "^7.5.5", + "mkdirp": "^1.0.4", + "mocha": "^9.1.3", + "react": "^16.2.0", + "react-dom": "^16.2.0", + "rimraf": "^3.0.2", + "terser": "^5.9.0" + }, + "peerDependencies": { + "react": ">=16.2.0", + "react-dom": ">=16.2.0" }, "bugs": { "url": "https://github.com/Automattic/interpolate-components/issues" @@ -32,11 +46,6 @@ "directories": { "test": "test" }, - "dependencies": { - "react": "^0.14.3 || ^15.1.0 || ^16.0.0", - "react-addons-create-fragment": "^0.14.3 || ^15.1.0", - "react-dom": "^0.14.3 || ^15.1.0 || ^16.0.0" - }, "author": "Bob Ralian (http://github.com/rralian)", "license": "GPL-2.0" } diff --git a/src/index.es6 b/src/index.es6 deleted file mode 100644 index f0d86f1..0000000 --- a/src/index.es6 +++ /dev/null @@ -1,127 +0,0 @@ -/** - * External Dependencies - */ -import React from 'react'; -import createFragment from 'react-addons-create-fragment'; - -/** - * Internal Dependencies - */ -import tokenize from './tokenize'; - -let currentMixedString; - -function getCloseIndex( openIndex, tokens ) { - var openToken = tokens[ openIndex ], - nestLevel = 0, - token, i; - for ( i = openIndex + 1; i < tokens.length; i++ ) { - token = tokens[ i ]; - if ( token.value === openToken.value ) { - if ( token.type === 'componentOpen' ) { - nestLevel++; - continue; - } - if ( token.type === 'componentClose' ) { - if ( nestLevel === 0 ) { - return i; - } - nestLevel--; - } - } - } - // if we get this far, there was no matching close token - throw new Error( 'Missing closing component token `' + openToken.value + '`' ); -} - -function buildChildren( tokens, components ) { - var children = [], - childrenObject = {}, - openComponent, clonedOpenComponent, openIndex, closeIndex, token, i, grandChildTokens, grandChildren, siblingTokens, siblings; - - for ( i = 0; i < tokens.length; i++ ) { - token = tokens[ i ]; - if ( token.type === 'string' ) { - children.push( token.value ); - continue; - } - // component node should at least be set - if ( ! components.hasOwnProperty( token.value ) || typeof components[ token.value ] === 'undefined' ) { - throw new Error( 'Invalid interpolation, missing component node: `' + token.value + '`' ); - } - // should be either ReactElement or null (both type "object"), all other types deprecated - if ( typeof components[ token.value ] !== 'object' ) { - throw new Error( 'Invalid interpolation, component node must be a ReactElement or null: `' + token.value + '`', '\n> ' + currentMixedString ); - } - // we should never see a componentClose token in this loop - if ( token.type === 'componentClose' ) { - throw new Error( 'Missing opening component token: `' + token.value + '`' ); - } - if ( token.type === 'componentOpen' ) { - openComponent = components[ token.value ]; - openIndex = i; - break; - } - // componentSelfClosing token - children.push( components[ token.value ] ); - continue; - } - - if ( openComponent ) { - closeIndex = getCloseIndex( openIndex, tokens ); - grandChildTokens = tokens.slice( ( openIndex + 1 ), closeIndex ); - grandChildren = buildChildren( grandChildTokens, components ); - clonedOpenComponent = React.cloneElement( openComponent, {}, grandChildren ); - children.push( clonedOpenComponent ); - - if ( closeIndex < tokens.length - 1 ) { - siblingTokens = tokens.slice( closeIndex + 1 ); - siblings = buildChildren( siblingTokens, components ); - children = children.concat( siblings ); - } - } - - if ( children.length === 1 ) { - return children[ 0 ]; - } - - children.forEach( ( child, index ) => { - if ( child ) { - childrenObject[ `interpolation-child-${index}` ] = child; - } - } ); - - return createFragment( childrenObject ); -} - -function interpolate( options ) { - const { mixedString, components, throwErrors } = options; - - currentMixedString = mixedString; - - if ( ! components ) { - return mixedString; - } - - if ( typeof components !== 'object' ) { - if ( throwErrors ) { - throw new Error( `Interpolation Error: unable to process \`${ mixedString }\` because components is not an object` ); - } - - return mixedString; - } - - let tokens = tokenize( mixedString ); - - try { - return buildChildren( tokens, components ); - } catch ( error ) { - if ( throwErrors ) { - throw new Error( `Interpolation Error: unable to process \`${ mixedString }\` because of error \`${ error.message }\`` ); - } - - return mixedString; - } -}; - -export default interpolate; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..f4cddd2 --- /dev/null +++ b/src/index.js @@ -0,0 +1,136 @@ +/** + * External Dependencies + */ +import React, { Fragment } from 'react'; + +/** + * Internal Dependencies + */ +import tokenize from './tokenize'; + +let currentMixedString; + +function getCloseIndex( openIndex, tokens ) { + const openToken = tokens[ openIndex ]; + let nestLevel = 0; + for ( let i = openIndex + 1; i < tokens.length; i++ ) { + const token = tokens[ i ]; + if ( token.value === openToken.value ) { + if ( token.type === 'componentOpen' ) { + nestLevel++; + continue; + } + if ( token.type === 'componentClose' ) { + if ( nestLevel === 0 ) { + return i; + } + nestLevel--; + } + } + } + // if we get this far, there was no matching close token + throw new Error( `Missing closing component token \`${ openToken.value }\`` ); +} + +function buildChildren( tokens, components ) { + let children = []; + let openComponent, openIndex; + + for ( let i = 0; i < tokens.length; i++ ) { + const token = tokens[ i ]; + if ( token.type === 'string' ) { + children.push( token.value ); + continue; + } + // component node should at least be set + if ( + ! components.hasOwnProperty( token.value ) || + typeof components[ token.value ] === 'undefined' + ) { + throw new Error( `Invalid interpolation, missing component node: \`${ token.value }\`` ); + } + // should be either ReactElement or null (both type "object"), all other types deprecated + if ( typeof components[ token.value ] !== 'object' ) { + throw new Error( + `Invalid interpolation, component node must be a ReactElement or null: \`${ token.value }\``, + '\n> ' + currentMixedString + ); + } + // we should never see a componentClose token in this loop + if ( token.type === 'componentClose' ) { + throw new Error( `Missing opening component token: \`${ token.value }\`` ); + } + if ( token.type === 'componentOpen' ) { + openComponent = components[ token.value ]; + openIndex = i; + break; + } + // componentSelfClosing token + children.push( components[ token.value ] ); + continue; + } + + if ( openComponent ) { + const closeIndex = getCloseIndex( openIndex, tokens ); + const grandChildTokens = tokens.slice( openIndex + 1, closeIndex ); + const grandChildren = buildChildren( grandChildTokens, components ); + const clonedOpenComponent = React.cloneElement( openComponent, {}, grandChildren ); + children.push( clonedOpenComponent ); + + if ( closeIndex < tokens.length - 1 ) { + const siblingTokens = tokens.slice( closeIndex + 1 ); + const siblings = buildChildren( siblingTokens, components ); + children = children.concat( siblings ); + } + } + + if ( children.length === 1 ) { + return children[ 0 ]; + } + + return ( + + { children.map( ( child, index ) => ( + { child } + ) ) } + + ); +} + +function interpolate( options ) { + const { mixedString, components, throwErrors } = options; + + currentMixedString = mixedString; + + if ( ! components ) { + return mixedString; + } + + if ( typeof components !== 'object' ) { + if ( throwErrors ) { + throw new Error( + `Interpolation Error: unable to process \`${ mixedString }\` because components is not an object` + ); + } + + return mixedString; + } + + const tokens = tokenize( mixedString ); + + try { + return buildChildren( tokens, components ); + } catch ( error ) { + if ( throwErrors ) { + throw new Error( + `Interpolation Error: unable to process \`${ mixedString }\` because of error \`${ + error.message + }\`` + ); + } + + return mixedString; + } +} + +export default interpolate; diff --git a/src/tokenize.es6 b/src/tokenize.js similarity index 100% rename from src/tokenize.es6 rename to src/tokenize.js