├── .gitignore ├── .babelrc ├── demo ├── css-module │ ├── style.scss │ ├── global-style.scss │ ├── index.jsx │ └── fruits.jsx.html ├── click-me │ ├── click-me-view.jsx.html │ ├── index.jsx │ └── click-me-container.jsx ├── todo-list │ ├── index.jsx │ ├── todo-view.jsx.html │ └── todo-container.jsx ├── import-export │ ├── index.jsx │ ├── user.scss │ ├── user.jsx.html │ ├── users-view.jsx.html │ └── users-container.jsx ├── index.html ├── index.js └── webpack.config.js ├── integration-tests ├── components │ ├── hello-world.jsx.html │ ├── props-spreading.jsx.html │ ├── hello-world.jsx │ ├── props-spreading.jsx │ ├── loops.jsx.html │ ├── loops.jsx │ ├── conditionals.jsx.html │ ├── imports.jsx │ ├── imports.jsx.html │ ├── conditionals.jsx │ ├── bindings.jsx.html │ └── bindings.jsx └── all.spec.js ├── .travis.yml ├── lib ├── validate-ast │ ├── index.spec.js │ ├── rules │ │ ├── root-elements.js │ │ ├── single-component-child.js │ │ ├── no-component-nesting.js │ │ ├── single-control-child.js │ │ ├── no-imports-in-components.js │ │ ├── single-default-component.js │ │ ├── style-imports-nesting.js │ │ ├── style-imports-attributes.js │ │ ├── loop-attributes.js │ │ ├── no-control-merge.js │ │ ├── imports-relation-types.js │ │ ├── components-attributes.js │ │ ├── loop-attributes.spec.js │ │ ├── style-imports-nesting.spec.js │ │ ├── root-elements.spec.js │ │ ├── no-component-nesting.spec.js │ │ ├── component-imports-nesting.js │ │ ├── no-imports-in-components.spec.js │ │ ├── single-component-child.spec.js │ │ ├── single-control-child.spec.js │ │ ├── components-attributes.spec.js │ │ ├── single-default-component.spec.js │ │ ├── imports-relation-types.spec.js │ │ ├── style-imports-attributes.spec.js │ │ ├── unique-ids.js │ │ ├── component-imports-attributes.js │ │ ├── no-control-merge.spec.js │ │ ├── component-imports-nesting.spec.js │ │ ├── unique-ids.spec.js │ │ └── component-imports-attributes.spec.js │ ├── result.js │ └── index.js ├── ast-to-react │ ├── index.js │ ├── extract-components.js │ ├── render-components.js │ ├── extract-components.spec.js │ ├── extract-imports.js │ ├── extract-imports.spec.js │ ├── render-component.js │ └── render-component.spec.js ├── constants.js ├── loggers.js ├── index.js ├── html-to-ast │ ├── index.spec.js │ └── index.js └── attribute-conversion.js ├── loader └── index.js ├── scripts ├── clean.js └── prepare-release.js ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "react", "es2015" ], 3 | "plugins": [ "transform-object-rest-spread" ] 4 | } 5 | -------------------------------------------------------------------------------- /demo/css-module/style.scss: -------------------------------------------------------------------------------- 1 | .fruit-list { 2 | margin: 0; 3 | } 4 | 5 | .item { 6 | font-style: italic; 7 | } 8 | -------------------------------------------------------------------------------- /integration-tests/components/hello-world.jsx.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /integration-tests/components/props-spreading.jsx.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /demo/click-me/click-me-view.jsx.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /demo/css-module/global-style.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #eee; 3 | font-family: Helvetica, Arial, sans-serif; 4 | color: #333; 5 | } 6 | 7 | :global(#app) { 8 | margin: 0 auto; 9 | } 10 | -------------------------------------------------------------------------------- /demo/css-module/index.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import Fruits from './fruits'; 6 | 7 | ReactDOM.render(, document.getElementById('app')); 8 | -------------------------------------------------------------------------------- /demo/todo-list/index.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import Todo from './todo-container'; 6 | 7 | ReactDOM.render(, document.getElementById('app')); 8 | -------------------------------------------------------------------------------- /demo/import-export/index.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import Users from './users-container'; 6 | 7 | ReactDOM.render(, document.getElementById('app')); 8 | -------------------------------------------------------------------------------- /integration-tests/components/hello-world.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | export default function(props) { 6 | return ( 7 |
8 | Hello World 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /demo/click-me/index.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import ClickMe from './click-me-container'; 6 | 7 | ReactDOM.render(, document.getElementById('app')); 8 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo react-pure-html-component-loader 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /integration-tests/components/props-spreading.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | export default function(props) { 6 | return ( 7 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.9.4" 4 | cache: 5 | directories: 6 | - node_modules 7 | install: 8 | - npm install 9 | env: 10 | - TEST_TYPE=lint 11 | - TEST_TYPE=unit-test 12 | - TEST_TYPE=integration-test 13 | script: 14 | - npm run $TEST_TYPE 15 | -------------------------------------------------------------------------------- /demo/import-export/user.scss: -------------------------------------------------------------------------------- 1 | .user { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | padding: 0.5rem 0.75rem; 6 | 7 | .image { 8 | border-radius: 50%; 9 | margin-right: 1rem; 10 | } 11 | 12 | .name { 13 | font-family: Helvetica; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/validate-ast/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const glob = require('glob'); 4 | const path = require('path'); 5 | 6 | const rules = glob.sync(path.join(__dirname, 'rules', '**', '*.spec.js')) 7 | .map(p => `./${path.relative(__dirname, p)}`); 8 | 9 | describe('validate-ast', function() { 10 | rules.forEach(test => require(test)()); 11 | }); 12 | -------------------------------------------------------------------------------- /demo/import-export/user.jsx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /demo/import-export/users-view.jsx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /integration-tests/components/loops.jsx.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /integration-tests/components/loops.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | export default function(props) { 6 | return ( 7 |
8 | { props.users.map(user => ( 9 |
10 | { user.name } 11 |
12 | )) } 13 | { (props.users) && ( 14 | props.users.map(user => ( 15 |
16 | { user.name } 17 |
18 | )) 19 | ) } 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /integration-tests/components/conditionals.jsx.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /demo/css-module/fruits.jsx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | -------------------------------------------------------------------------------- /integration-tests/components/imports.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import AComponent from 'path/to/component/a'; 5 | import DefaultComponent, { NamedComponentOne, NamedComponentTwo as NamedComponentTwoAlias } from 'path/to/components/a'; 6 | import style from './style'; 7 | import './global-style'; 8 | 9 | export default function(props) { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /loader/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const htmlToReact = require('../lib'); 4 | 5 | function formatMessage(obj) { 6 | if (typeof obj.line !== 'undefined') { 7 | return ` Line ${obj.line}: ${obj.message}`; 8 | } 9 | return ` ${obj.message}`; 10 | } 11 | 12 | module.exports = function(content) { 13 | this.cacheable(); 14 | 15 | const res = htmlToReact({ html: content }); 16 | 17 | res.errors.forEach(err => { 18 | this.emitError(formatMessage(err)); 19 | }); 20 | 21 | res.warnings.forEach(warn => { 22 | this.emitWarning(formatMessage(warn)); 23 | }); 24 | 25 | return res.reactStr; 26 | }; 27 | -------------------------------------------------------------------------------- /demo/todo-list/todo-view.jsx.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 25 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/root-elements.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | module.exports = function(ast) { 7 | const mapper = node => { 8 | if (node.name !== constants.tags.COMPONENT 9 | && node.name !== constants.tags.IMPORT) { 10 | return result.withError( 11 | `Only '<${constants.tags.COMPONENT}>' and '<${constants.tags.IMPORT}>'` 12 | + ` are allowed as root elements`, 13 | node.meta.line 14 | ); 15 | } 16 | return result.empty(); 17 | }; 18 | 19 | return ast.map(mapper) 20 | .reduce(result.reducer, result.empty()); 21 | }; 22 | -------------------------------------------------------------------------------- /scripts/clean.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: "off" */ 2 | 'use strict'; 3 | 4 | const del = require('del'); 5 | const path = require('path'); 6 | 7 | function absPath() { 8 | return path.join(__dirname, '..', ...arguments); 9 | } 10 | 11 | const filesToDelete = [ absPath('dist') ]; 12 | 13 | del(filesToDelete) 14 | .then(files => { 15 | if (files.length > 0) { 16 | files = files.map(file => path.relative(absPath(''), file)); 17 | console.log(`Deleted paths:\n\n ${files.join('\n ')}\n`); 18 | } else { 19 | console.log('Already clean'); 20 | } 21 | }) 22 | .catch(err => { 23 | console.error(err); 24 | process.exit(1); 25 | }); 26 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/single-component-child.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | function singleChild(node) { 7 | if (node.children.length === 1) { 8 | return result.empty(); 9 | } 10 | 11 | return result.withError( 12 | `A '<${constants.tags.COMPONENT}>' must have exactly one child, found` 13 | + ` ${node.children.length}`, 14 | node.meta.line 15 | ); 16 | } 17 | 18 | module.exports = function(ast) { 19 | return ast 20 | .filter(node => node.name === constants.tags.COMPONENT) 21 | .map(singleChild) 22 | .reduce(result.reducer, result.empty()); 23 | }; 24 | -------------------------------------------------------------------------------- /integration-tests/components/imports.jsx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /integration-tests/components/conditionals.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | export function ConditionalFist(props) { 6 | return ( 7 | (props.user) && ( 8 |
9 | { props.user.name } 10 |
11 | ) 12 | ); 13 | } 14 | ConditionalFist.displayName = 'ConditionalFist'; 15 | 16 | export default function(props) { 17 | return ( 18 |
19 | { (props.user) && ( 20 |
21 | { props.user.name } 22 |
23 | ) } 24 | { props.array.map(item => ( 25 | (item.isValid) && ( 26 |
27 | { item.name } 28 |
29 | ) 30 | )) } 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /demo/import-export/users-container.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component } from 'react'; 4 | import axios from 'axios'; 5 | 6 | import UsersView from './users-view'; 7 | 8 | export default class UsersContainer extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { users: [] }; 13 | } 14 | 15 | componentDidMount() { 16 | this.fetchUsers(); 17 | } 18 | 19 | fetchUsers() { 20 | axios('https://randomuser.me/api?results=100&seed=react') 21 | .then(res => this.setState({ users: res.data.results })); 22 | } 23 | 24 | render() { 25 | return ( 26 | 27 | ); 28 | } 29 | } 30 | 31 | UsersContainer.displayName = 'UsersContainer'; 32 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/no-component-nesting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | function noComponentNesting(children) { 7 | const mapper = child => { 8 | if (child.name === constants.tags.COMPONENT) { 9 | return result.withError( 10 | `A '<${constants.tags.COMPONENT}>' cannot be nested`, 11 | child.meta.line 12 | ); 13 | } 14 | return noComponentNesting(child.children); 15 | }; 16 | 17 | return children.map(mapper) 18 | .reduce(result.reducer, result.empty()); 19 | } 20 | 21 | module.exports = function(ast) { 22 | return ast.map(root => noComponentNesting(root.children)) 23 | .reduce(result.reducer, result.empty()); 24 | }; 25 | -------------------------------------------------------------------------------- /demo/click-me/click-me-container.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component } from 'react'; 4 | 5 | import ClickMeView from './click-me-view'; 6 | 7 | export default class ClickMeContainer extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { clicks: 0 }; 12 | this.buttonProps = { onMouseDown: this.handleMouseDown.bind(this) }; 13 | } 14 | 15 | handleMouseDown(e) { 16 | e.preventDefault(); 17 | this.setState({ clicks: this.state.clicks + 1 }); 18 | } 19 | 20 | render() { 21 | return ( 22 | 26 | ); 27 | } 28 | } 29 | 30 | ClickMeContainer.displayName = 'ClickMeContainer'; 31 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/single-control-child.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | function singleControlChild(node) { 7 | let res = result.empty(); 8 | 9 | if (node.name === constants.tags.CONTROLS && node.children.length !== 1) { 10 | res = result.withError( 11 | `A '<${constants.tags.CONTROLS}>' must have exactly one child, found` 12 | + ` ${node.children.length}`, 13 | node.meta.line 14 | ); 15 | } 16 | 17 | return node.children 18 | .map(singleControlChild) 19 | .reduce(result.reducer, res); 20 | } 21 | 22 | module.exports = function(ast) { 23 | return ast 24 | .map(singleControlChild) 25 | .reduce(result.reducer, result.empty()); 26 | }; 27 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/no-imports-in-components.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | function noImportInComponents(node) { 7 | const mapper = child => { 8 | if (child.name === constants.tags.IMPORT) { 9 | return result.withError( 10 | 'Imports should be at the top level of the file', 11 | child.meta.line 12 | ); 13 | } 14 | return noImportInComponents(child); 15 | }; 16 | 17 | return node.children 18 | .map(mapper) 19 | .reduce(result.reducer, result.empty()); 20 | } 21 | 22 | module.exports = function(ast) { 23 | return ast 24 | .filter(node => node.name === constants.tags.COMPONENT) 25 | .map(noImportInComponents) 26 | .reduce(result.reducer, result.empty()); 27 | }; 28 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/single-default-component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | const componentsAttrs = constants.attributes.components; 7 | 8 | function onlyDefault(node) { 9 | return node.name === constants.tags.COMPONENT 10 | && node.attrs.hasOwnProperty(componentsAttrs.DEFAULT); 11 | } 12 | 13 | module.exports = function(ast) { 14 | const defaultComponents = ast.filter(onlyDefault); 15 | 16 | if (defaultComponents.length > 1) { 17 | const mapper = node => result.withError( 18 | `Only one default component is allowed`, 19 | node.meta.line 20 | ); 21 | 22 | return defaultComponents 23 | .map(mapper) 24 | .reduce(result.reducer, result.empty()); 25 | } 26 | 27 | return result.empty(); 28 | }; 29 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/style-imports-nesting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | const importAttrs = constants.attributes.import; 7 | 8 | function importSubTree(node) { 9 | if (node.children.length > 0) { 10 | return result.withError( 11 | `A style import cannot have children, found ${node.children.length}`, 12 | node.meta.line 13 | ); 14 | } 15 | 16 | return result.empty(); 17 | } 18 | 19 | function importsNesting(ast) { 20 | return ast 21 | .filter(node => ( 22 | node.name === constants.tags.IMPORT 23 | && node.attrs[importAttrs.TYPE] === importAttrs.types.STYLESHEET 24 | )) 25 | .map(importSubTree) 26 | .reduce(result.reducer, result.empty()); 27 | } 28 | 29 | module.exports = importsNesting; 30 | -------------------------------------------------------------------------------- /lib/ast-to-react/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const extractImports = require('./extract-imports'); 4 | const extractComponents = require('./extract-components'); 5 | const renderComponents = require('./render-components'); 6 | 7 | function astToReact(ast) { 8 | const imports = extractImports(ast.roots); 9 | const components = extractComponents(ast.roots); 10 | const renderedComponents = renderComponents({ 11 | namedNodes: components.namedNodes, 12 | defaultNode: components.defaultNode, 13 | tagToVar: Object.assign({}, imports.tagToVar, components.tagToVar) 14 | }); 15 | 16 | let content = `'use strict';\n\nimport React from 'react';\n`; 17 | content = `${content}${imports.rendered}\n`; 18 | content = `${content}${renderedComponents.join('\n')}`; 19 | return content; 20 | } 21 | 22 | module.exports = astToReact; 23 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/style-imports-attributes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | const importAttrs = constants.attributes.import; 7 | 8 | function importAttributes(node) { 9 | if (!node.attrs[importAttrs.PATH]) { 10 | return result.withError( 11 | `A style import must have the attribute '${importAttrs.PATH}'`, 12 | node.meta.line 13 | ); 14 | } 15 | 16 | return result.empty(); 17 | } 18 | 19 | function importsAttributes(ast) { 20 | return ast 21 | .filter(node => ( 22 | node.name === constants.tags.IMPORT 23 | && node.attrs[importAttrs.TYPE] === importAttrs.types.STYLESHEET 24 | )) 25 | .map(importAttributes) 26 | .reduce(result.reducer, result.empty()); 27 | } 28 | 29 | module.exports = importsAttributes; 30 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/loop-attributes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | const controlAttrs = constants.attributes.controls; 7 | 8 | function loopAttributes(node) { 9 | let res = result.empty(); 10 | 11 | if (node.name === constants.tags.CONTROLS 12 | && node.attrs.hasOwnProperty(controlAttrs.LOOP_ARRAY) 13 | && !node.attrs[controlAttrs.LOOP_VAR_NAME]) { 14 | res = result.withError( 15 | `A loop must have the attribute '${controlAttrs.LOOP_VAR_NAME}'`, 16 | node.meta.line 17 | ); 18 | } 19 | 20 | return node.children 21 | .map(loopAttributes) 22 | .reduce(result.reducer, res); 23 | } 24 | 25 | module.exports = function(ast) { 26 | return ast 27 | .map(loopAttributes) 28 | .reduce(result.reducer, result.empty()); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/no-control-merge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | const controlAttrs = constants.attributes.controls; 7 | 8 | function noControlMerge(node) { 9 | let res = result.empty(); 10 | 11 | if (node.name === constants.tags.CONTROLS 12 | && (node.attrs.hasOwnProperty(controlAttrs.LOOP_ARRAY) 13 | === node.attrs.hasOwnProperty(controlAttrs.CONDITIONALS_TEST))) { 14 | res = result.withError( 15 | `A '<${constants.tags.CONTROLS}>' must be either a conditional or a loop`, 16 | node.meta.line 17 | ); 18 | } 19 | 20 | return node.children 21 | .map(noControlMerge) 22 | .reduce(result.reducer, res); 23 | } 24 | 25 | module.exports = function(ast) { 26 | return ast 27 | .map(noControlMerge) 28 | .reduce(result.reducer, result.empty()); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/imports-relation-types.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | const importAttrs = constants.attributes.import; 7 | const types = Object.keys(importAttrs.types) 8 | .map(k => importAttrs.types[k]); 9 | const typesStr = types.map(t => `'${t}'`).join(', '); 10 | 11 | function hasValidRelationType(node) { 12 | const importType = node.attrs[importAttrs.TYPE]; 13 | 14 | if (types.some(type => type === importType)) { 15 | return result.empty(); 16 | } 17 | 18 | return result.withError( 19 | `Import type '${importType}' is not valid, should be one of: ${typesStr}`, 20 | node.meta.line 21 | ); 22 | } 23 | 24 | module.exports = function(ast) { 25 | return ast 26 | .filter(node => node.name === constants.tags.IMPORT) 27 | .map(hasValidRelationType) 28 | .reduce(result.reducer, result.empty()); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = Object.freeze({ 4 | tags: { 5 | IMPORT: 'link', 6 | COMPONENT: 'template', 7 | CONTROLS: 'render' 8 | }, 9 | 10 | attributes: { 11 | import: { 12 | TYPE: 'rel', 13 | PATH: 'href', 14 | NAMED: 'name', 15 | ALIAS: 'id', 16 | OBJECT_VALUE: 'id', 17 | 18 | types: { 19 | COMPONENT: 'import', 20 | STYLESHEET: 'stylesheet' 21 | } 22 | }, 23 | 24 | controls: { 25 | LOOP_ARRAY: 'for-each', 26 | LOOP_VAR_NAME: 'as', 27 | CONDITIONALS_TEST: 'if' 28 | }, 29 | 30 | components: { 31 | ID: 'id', 32 | DEFAULT: 'default' 33 | }, 34 | 35 | PROPS_SPREADING: 'use-props', 36 | 37 | bindings: { 38 | PATTERN: /{{\s*((?:(?!(?:{{|}})).)*?)\s*}}/g, 39 | STRICT_PATTERN: /^\s*{{\s*((?:(?!(?:{{|}})).)*?)\s*}}\s*$/, 40 | BOLLEAN_PATTERN: /^\s*(true|false)\s*$/i 41 | } 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /lib/validate-ast/result.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function createResult(errors, warnings) { 4 | errors = errors || []; 5 | warnings = warnings || []; 6 | 7 | return { 8 | errors, 9 | warnings, 10 | concat, 11 | build 12 | }; 13 | } 14 | 15 | function concat(other) { 16 | return createResult( 17 | this.errors.concat(other.errors), 18 | this.warnings.concat(other.warnings) 19 | ); 20 | } 21 | 22 | function build() { 23 | return { errors: this.errors, warnings: this.warnings }; 24 | } 25 | 26 | function empty() { 27 | return createResult(); 28 | } 29 | 30 | function withError(message, line) { 31 | return createResult([{ message, line }]); 32 | } 33 | 34 | function withWarning(message, line) { 35 | return createResult([], [{ message, line }]); 36 | } 37 | 38 | function reducer(acc, res) { 39 | return acc.concat(res); 40 | } 41 | 42 | module.exports = { 43 | empty, 44 | withError, 45 | withWarning, 46 | reducer 47 | }; 48 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/components-attributes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('../../constants'); 4 | const result = require('../result'); 5 | 6 | const componentAttrs = constants.attributes.components; 7 | 8 | function componentAttributes(node) { 9 | if (node.attrs[componentAttrs.ID]) { 10 | return result.empty(); 11 | } 12 | 13 | if (node.attrs.hasOwnProperty(componentAttrs.DEFAULT)) { 14 | return result.withWarning( 15 | `Concider adding an attribute '${componentAttrs.ID}' to a default` 16 | + ` component for debug purposes`, 17 | node.meta.line 18 | ); 19 | } 20 | 21 | return result.withError( 22 | `A named component must have an attribute '${componentAttrs.ID}' to be used`, 23 | node.meta.line 24 | ); 25 | } 26 | 27 | module.exports = function(ast) { 28 | return ast 29 | .filter(node => node.name === constants.tags.COMPONENT) 30 | .map(componentAttributes) 31 | .reduce(result.reducer, result.empty()); 32 | }; 33 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:"off" */ 2 | 'use strict'; 3 | 4 | const minimistOptions = { 5 | alias: { port: 'p' }, 6 | default: { port: 8080 } 7 | }; 8 | 9 | const argv = require('minimist')(process.argv.slice(2), minimistOptions); 10 | const path = require('path'); 11 | const webpack = require('webpack'); 12 | const WebpackDevServer = require('webpack-dev-server'); 13 | 14 | const webpackConfig = require('./webpack.config.js'); 15 | 16 | let demo = path.join('demo', 'todo-list'); 17 | if (argv._.length === 1) { 18 | demo = argv._[0]; 19 | } 20 | 21 | const config = webpackConfig({ 22 | port: argv.port, 23 | outputDir: path.resolve(), 24 | entry: path.join(process.env.PWD, demo, 'index.jsx'), 25 | html: path.join(__dirname, 'index.html') 26 | }); 27 | 28 | const devServerOptions = { 29 | stats: { colors: true } 30 | }; 31 | 32 | new WebpackDevServer(webpack(config), devServerOptions) 33 | .listen(argv.port, '0.0.0.0', err => { 34 | if (err) { 35 | console.error(err); 36 | process.exit(1); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /lib/loggers.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: "off" */ 2 | 'use strict'; 3 | 4 | const NONE = 10; 5 | const LOG = 1; 6 | const WARNING = 2; 7 | const ERROR = 3; 8 | 9 | function log(level) { 10 | return function() { 11 | if (level <= LOG) { 12 | console.log.apply(console, arguments); 13 | } 14 | }; 15 | } 16 | 17 | function warn(level) { 18 | return function() { 19 | if (level <= WARNING) { 20 | console.warn.apply(console, arguments); 21 | } 22 | }; 23 | } 24 | 25 | function error(level) { 26 | return function() { 27 | if (level <= ERROR) { 28 | console.error.apply(console, arguments); 29 | } 30 | }; 31 | } 32 | 33 | function str(obj) { 34 | return JSON.stringify(obj, null, ' '); 35 | } 36 | 37 | module.exports = function(level) { 38 | level = level || NONE; 39 | return { 40 | log: log(level), 41 | warn: warn(level), 42 | error: error(level) 43 | }; 44 | }; 45 | 46 | module.exports.str = str; 47 | module.exports.levels = { 48 | NONE, 49 | LOG, 50 | WARNING, 51 | ERROR 52 | }; 53 | -------------------------------------------------------------------------------- /demo/todo-list/todo-container.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, { Component } from 'react'; 4 | 5 | import TodoView from './todo-view'; 6 | 7 | export default class Todo extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { todos: [], value: '' }; 12 | this.inputProps = { onChange: this.handleValueChanged.bind(this) }; 13 | this.formProps = { onSubmit: this.handleAddTodo.bind(this) }; 14 | } 15 | 16 | handleValueChanged(e) { 17 | e.preventDefault(); 18 | this.setState({ value: e.target.value }); 19 | } 20 | 21 | handleAddTodo(e) { 22 | e.preventDefault(); 23 | 24 | const { todos, value } = this.state; 25 | this.setState({ todos: todos.concat(value), value: '' }); 26 | } 27 | 28 | render() { 29 | const { todos, value } = this.state; 30 | return ( 31 | 36 | ); 37 | } 38 | } 39 | 40 | Todo.displayName = 'Todo'; 41 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/loop-attributes.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: "off" */ 2 | 'use strict'; 3 | 4 | const chai = require('chai'); 5 | const expect = chai.expect; 6 | 7 | const loopAttributes = require('./loop-attributes'); 8 | const result = require('../result'); 9 | 10 | module.exports = function() { 11 | describe('loop-attributes', function() { 12 | it(`should accept ''`, function() { 13 | const ast = [{ 14 | name: 'render', 15 | attrs: { 'for-each': 'hello', 'as': 'wat' }, 16 | children: [] 17 | }]; 18 | const res = loopAttributes(ast).build(); 19 | 20 | expect(res).to.deep.equal(result.empty().build()); 21 | }); 22 | 23 | it(`should reject missing attribute 'as' in ''`, function() { 24 | const ast = [{ 25 | name: 'render', 26 | attrs: { 'for-each': '' }, 27 | children: [], 28 | meta: {} 29 | }]; 30 | const res = loopAttributes(ast).build(); 31 | 32 | expect(res.errors.length).to.equal(1); 33 | }); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /integration-tests/all.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const glob = require('glob'); 4 | const path = require('path'); 5 | const fs = require('fs-promise'); 6 | const chai = require('chai'); 7 | const reactHtmlTemplate = require('../lib'); 8 | 9 | const expect = chai.expect; 10 | const pattern = `${__dirname}/components/**/*.jsx.html`; 11 | const components = glob.sync(pattern).map(f => ({ 12 | srcPath: f, 13 | expectedPath: f.replace('.html', '') 14 | })); 15 | 16 | describe('E2E tests', function() { 17 | components.forEach(template => { 18 | const name = path.basename(template.srcPath, '.jsx.html'); 19 | const contents = [ 20 | fs.readFile(template.srcPath, 'utf8'), 21 | fs.readFile(template.expectedPath, 'utf8') 22 | ]; 23 | const test = (html, expected) => { 24 | const res = reactHtmlTemplate({ html }); 25 | expect(res.reactStr).to.equal(expected); 26 | }; 27 | 28 | it(name, function(done) { 29 | Promise.all(contents) 30 | .then(res => test(...res)) 31 | .then(done) 32 | .catch(done); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/style-imports-nesting.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: "off" */ 2 | 'use strict'; 3 | 4 | const chai = require('chai'); 5 | const expect = chai.expect; 6 | 7 | const importsNesting = require('./style-imports-nesting'); 8 | const result = require('../result'); 9 | 10 | module.exports = function() { 11 | describe('style-imports-nesting', function() { 12 | it('should accept absence of child', function() { 13 | const ast = [{ name: 'link', attrs: { rel: 'stylesheet' }, children: [] }]; 14 | const res = importsNesting(ast).build(); 15 | 16 | expect(res).to.deep.equal(result.empty().build()); 17 | }); 18 | 19 | it('should reject any children elements', function() { 20 | const ast = [{ 21 | name: 'link', 22 | attrs: { rel: 'stylesheet' }, 23 | children: [ 24 | { name: 'div', children: [], meta: {} }, 25 | { name: 'div', children: [], meta: {} } 26 | ], 27 | meta: {} 28 | }]; 29 | const res = importsNesting(ast).build(); 30 | 31 | expect(res.errors.length).to.equal(1); 32 | }); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Simon Relet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/validate-ast/rules/root-elements.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | 6 | const rootElements = require('./root-elements'); 7 | const result = require('../result'); 8 | 9 | module.exports = function() { 10 | describe('root-elements', function() { 11 | it(`should accept '