1 | {% unknown_tag %}
+ | ^^^^^^^^^^^
+
+Expected a known tag such as
+- "
+`;
+
+exports[`Parser when parsing tags should match tags 1`] = `
+Object {
+ "expressions": Array [
+ Object {
+ "alternate": Object {
+ "expressions": Array [
+ Object {
+ "type": "PrintTextStatement",
+ "value": Object {
+ "type": "StringLiteral",
+ "value": "hello universe",
+ },
+ },
+ ],
+ "type": "SequenceExpression",
+ },
+ "consequent": Object {
+ "expressions": Array [
+ Object {
+ "type": "PrintTextStatement",
+ "value": Object {
+ "type": "StringLiteral",
+ "value": "hello ",
+ },
+ },
+ Object {
+ "type": "PrintExpressionStatement",
+ "value": Object {
+ "name": "adjective",
+ "type": "Identifier",
+ },
+ },
+ Object {
+ "type": "PrintTextStatement",
+ "value": Object {
+ "type": "StringLiteral",
+ "value": " world",
+ },
+ },
+ ],
+ "type": "SequenceExpression",
+ },
+ "test": Object {
+ "name": "foo",
+ "type": "Identifier",
+ },
+ "type": "ConditionalExpression",
+ },
+ ],
+ "type": "SequenceExpression",
+}
+`;
diff --git a/packages/melody-compiler/package.json b/packages/melody-compiler/package.json
new file mode 100644
index 0000000..3d530c9
--- /dev/null
+++ b/packages/melody-compiler/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "melody-compiler",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "babel-generator": "6.10.x",
+ "babel-template": "^6.8.0",
+ "babel-types": "^6.8.1",
+ "lodash": "^4.12.0",
+ "melody-code-frame": "^0.11.1-rc.1",
+ "random-seed": "^0.3.0"
+ },
+ "peerDependencies": {
+ "melody-idom": "^0.10.0",
+ "melody-parser": "^0.10.0",
+ "melody-runtime": "^0.10.0",
+ "melody-traverse": "^0.10.0",
+ "melody-types": "^0.10.0"
+ },
+ "bundledDependencies": [
+ "babel-types",
+ "babel-generator",
+ "babel-template",
+ "random-seed"
+ ],
+ "devDependencies": {
+ "melody-extension-core": "^0.11.1-rc.1",
+ "melody-plugin-idom": "^0.11.1-rc.1",
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-compiler/src/Template.js b/packages/melody-compiler/src/Template.js
new file mode 100644
index 0000000..3019883
--- /dev/null
+++ b/packages/melody-compiler/src/Template.js
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Node, type, alias, visitor } from 'melody-types';
+
+export class Template extends Node {
+ constructor(body = null) {
+ super();
+ this.parentName = null;
+ this.body = body;
+ this.macros = [];
+ this.blocks = [];
+ this.useImports = [];
+ }
+}
+type(Template, 'Template');
+alias(Template, 'Scope');
+visitor(Template, 'parentName', 'macros', 'blocks', 'useImports', 'body');
+
+export class File extends Node {
+ constructor(fileName, template) {
+ super();
+ this.template = template;
+ this.fileName = fileName;
+ }
+}
+type(File, 'File');
+//alias(File, 'Scope');
+visitor(File, 'template');
diff --git a/packages/melody-compiler/src/analyse/extractTemplateInfo.js b/packages/melody-compiler/src/analyse/extractTemplateInfo.js
new file mode 100644
index 0000000..314dbb2
--- /dev/null
+++ b/packages/melody-compiler/src/analyse/extractTemplateInfo.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*eslint no-unused-vars: "off"*/
+import { BlockCallExpression } from 'melody-extension-core';
+import {
+ StringLiteral,
+ FilterExpression,
+ MemberExpression,
+ NumericLiteral,
+ Identifier,
+} from 'melody-types';
+
+export default {
+ ExtendsStatement(path) {
+ const parentName = path.node.parentName;
+ path.remove();
+ this.template.parentName = parentName;
+ },
+ FlushStatement(path) {
+ // we don't have any use for flush statements
+ path.remove();
+ },
+ SliceExpression(path) {
+ path.replaceWith(
+ new FilterExpression(path.node.target, new Identifier('slice'), [
+ path.node.start || new NumericLiteral(0),
+ path.node.end ||
+ new MemberExpression(
+ path.node.target,
+ new Identifier('length'),
+ ),
+ ]),
+ );
+ },
+};
diff --git a/packages/melody-compiler/src/analyse/index.js b/packages/melody-compiler/src/analyse/index.js
new file mode 100644
index 0000000..62e3a54
--- /dev/null
+++ b/packages/melody-compiler/src/analyse/index.js
@@ -0,0 +1,20 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { merge } from 'melody-traverse';
+import extractTemplateInfo from './extractTemplateInfo';
+import analyseScope from './scope';
+
+export default merge(analyseScope, extractTemplateInfo);
diff --git a/packages/melody-compiler/src/analyse/scope.js b/packages/melody-compiler/src/analyse/scope.js
new file mode 100644
index 0000000..3a414b8
--- /dev/null
+++ b/packages/melody-compiler/src/analyse/scope.js
@@ -0,0 +1,155 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export default {
+ Template: {
+ enter(path) {
+ if (!path.node.parentName) {
+ const contextName = path.scope.generateUid('context');
+ path.scope.registerBinding(contextName, path, 'param');
+ path.scope.contextName = contextName;
+ }
+ },
+ exit(path) {
+ if (path.node.parentName) {
+ const body = path.get('body');
+ for (let i = 0, len = body.length; i < len; i++) {
+ const stmt = body[i];
+ if (
+ !stmt.is('ContextMutation') &&
+ !stmt.is('BlockStatement')
+ ) {
+ stmt.remove();
+ }
+ }
+ }
+ },
+ },
+ Identifier(path) {
+ if (this.isReferenceIdentifier(path)) {
+ path.scope.reference(path.node.name, path);
+ } else if (
+ path.parentPath.is('MacroDeclarationStatement') &&
+ path.parentKey == 'arguments'
+ ) {
+ path.scope.registerBinding(path.node.name, path, 'param');
+ } else if (
+ path.parentPath.is('CallExpression') &&
+ path.parentKey === 'callee'
+ ) {
+ if (this.functionMap[path.node.name]) {
+ path.scope.registerBinding(path.node.name, path, 'function');
+ } else {
+ path.scope.reference(path.node.name, path);
+ }
+ }
+ },
+ VariableDeclarationStatement(path) {
+ const varName = path.node.name;
+ const previousBinding = path.scope.getBinding(varName.name);
+ if (previousBinding && previousBinding.kind === 'var') {
+ previousBinding.definitionPaths.push(path);
+ previousBinding.mutated = true;
+ } else {
+ path.scope.registerBinding(
+ varName.name,
+ path,
+ 'var',
+ ).contextual = true;
+ path.scope.mutated = true;
+ }
+ },
+ FromStatement: {
+ exit(path) {
+ if (path.get('source').is('Identifier')) {
+ if (path.node.source.name === '_self') {
+ path.remove();
+ }
+ }
+ },
+ },
+ ImportDeclaration(path) {
+ const rootScope = path.scope,
+ identifier = path.node.alias || path.node.key;
+ const binding = rootScope.registerBinding(
+ identifier.name,
+ path.node.alias ? path.get('alias') : path.get('key'),
+ 'macro',
+ );
+ if (
+ path.get('key').is('Identifier') &&
+ path.node.key.name !== '_self'
+ ) {
+ binding.setData('Identifier.OriginalName', path.node.key.name);
+ }
+ },
+ MacroDeclarationStatement(path) {
+ const scope = path.scope;
+ const rootScope = scope.getRootScope();
+ rootScope.registerBinding(
+ path.node.name.name,
+ path.get('name'),
+ 'macro',
+ );
+
+ scope.registerBinding('varargs', path, 'param');
+ },
+ Include(path) {
+ if (path.node.contextFree === false) {
+ path.scope.escapesContext = true;
+ }
+ },
+ BlockStatement(path) {
+ if (this.template.parentName || path.parentPath.is('EmbedStatement')) {
+ path.scope.registerBinding('parent', path, 'var');
+ }
+ path.scope.registerBinding('_context', path, 'param');
+ path.scope.contextName = '_context';
+ path.parentPath.scope.escapesContext = true;
+ },
+ Scope: {
+ exit(path) {
+ if (path.scope.escapesContext) {
+ if (path.scope.parent) {
+ if (!path.scope.parent.escapesContext) {
+ path.scope.parent.escapesContext = true;
+ }
+ }
+ }
+ if (
+ path.scope.mutated &&
+ path.scope.escapesContext &&
+ path.scope.contextName === '_context'
+ ) {
+ const contextName = path.scope.generateUid('context');
+ path.scope.registerBinding(contextName, path, 'const');
+ path.scope.contextName = contextName;
+ }
+ },
+ },
+ RootScope: {
+ exit(path) {
+ if (
+ path.scope.mutated &&
+ path.scope.escapesContext &&
+ path.scope.contextName === '_context'
+ ) {
+ const contextName = path.scope.generateUid('context');
+ path.scope.registerBinding(contextName, path, 'const');
+ path.scope.contextName = contextName;
+ }
+ },
+ },
+};
diff --git a/packages/melody-compiler/src/convert/blocks.js b/packages/melody-compiler/src/convert/blocks.js
new file mode 100644
index 0000000..f028360
--- /dev/null
+++ b/packages/melody-compiler/src/convert/blocks.js
@@ -0,0 +1,393 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+import { Fragment } from 'melody-types';
+import template from 'babel-template';
+
+const buildRenderFunction = template(`
+TEMPLATE.NAME = function(_context) {
+ BODY
+};
+`);
+
+export default {
+ FromStatement(path) {
+ const fromStmt = path.node,
+ source = fromStmt.source;
+ for (const { key, alias } of (fromStmt.imports: Array)) {
+ this.addImportFrom(source.value, key.name, alias.name);
+ }
+ path.remove();
+ },
+ ImportDeclaration: {
+ exit(path) {
+ const { key, alias } = path.node;
+ const keyPath = path.get('key');
+ const parentPath = path.parentPath;
+ if (
+ !parentPath.is('MacroDeclaration') &&
+ !parentPath.is('Template') &&
+ !parentPath.is('BlockStatement')
+ ) {
+ throw new Error(
+ 'Import must be used in macro, block or template',
+ );
+ }
+ if (!keyPath.is('StringLiteral')) {
+ if (!(keyPath.is('Identifier') && key.name === '_self')) {
+ // todo: proper error reporting
+ throw Error();
+ } else {
+ const selfBinding = path.scope.getBinding(
+ path.node.alias.name,
+ ),
+ macroNames = [];
+ for (const usagePath of selfBinding.referencePaths) {
+ const boundName =
+ usagePath.parentPath.node.property.name;
+ macroNames.push(
+ t.objectProperty(
+ t.identifier(boundName),
+ t.identifier(boundName),
+ false,
+ true,
+ ),
+ );
+ }
+ path.scope.block.body.unshift(
+ t.variableDeclaration('const', [
+ t.variableDeclarator(
+ t.identifier(selfBinding.identifier),
+ t.objectExpression(macroNames),
+ ),
+ ]),
+ );
+ }
+ } else {
+ this.addNamespaceImportFrom(key.value, alias.name);
+ }
+ path.remove();
+ },
+ },
+ MacroDeclarationStatement: {
+ exit(path) {
+ const { node, scope } = path;
+ const args = [...node.arguments];
+ if (scope.getBinding('varargs').referenced) {
+ args.push(t.restElement(t.identifier('varargs')));
+ }
+ const macroStmt = t.exportNamedDeclaration(
+ t.functionDeclaration(node.name, args, node.body),
+ [],
+ );
+ path.remove();
+ this.program.body.push(macroStmt);
+ },
+ },
+ UseStatement(path) {
+ const useStmt = path.node,
+ source = useStmt.source;
+ if (useStmt.aliases.length) {
+ const members = [];
+ for (const { name, alias } of (useStmt.aliases: Array)) {
+ const aliasName = toBlockName(alias.name),
+ nameName = toBlockName(name.name);
+ this.addImportFrom(source.value, aliasName, nameName);
+ members.push(
+ t.objectProperty(
+ t.identifier(aliasName),
+ t.identifier(aliasName),
+ false,
+ true,
+ ),
+ );
+ }
+ const inheritBlocks = t.expressionStatement(
+ t.callExpression(
+ t.memberExpression(
+ t.identifier('Object'),
+ t.identifier('assign'),
+ ),
+ [
+ t.identifier(this.templateVariableName),
+ t.objectExpression(members),
+ ],
+ ),
+ );
+ this.program.body.push(inheritBlocks);
+ } else {
+ const name = this.addDefaultImportFrom(
+ source.value,
+ path.scope.generateUid('use'),
+ );
+ this.program.body.push(
+ t.expressionStatement(
+ t.callExpression(
+ t.identifier(
+ this.addImportFrom(
+ 'melody-runtime',
+ 'inheritBlocks',
+ ),
+ ),
+ [
+ t.identifier(this.templateVariableName),
+ t.identifier(name),
+ ],
+ ),
+ ),
+ );
+ }
+ path.remove();
+ },
+ CallExpression: {
+ exit(path) {
+ const callee = path.get('callee');
+ if (callee.is('Identifier') && callee.node.name === 'block') {
+ path.replaceWith(
+ t.expressionStatement(
+ t.callExpression(
+ t.memberExpression(
+ t.identifier('this'),
+ t.identifier(
+ toBlockName(
+ path.get('arguments')[0].node.value,
+ ),
+ ),
+ ),
+ [t.identifier(path.scope.contextName)],
+ ),
+ ),
+ );
+ } else if (
+ callee.is('MemberExpression') &&
+ callee.get('object').is('Identifier')
+ ) {
+ const name = callee.get('object').node.name;
+ const binding = path.scope.getBinding(name);
+ if (binding && binding.kind === 'macro') {
+ path.replaceWithJS(t.expressionStatement(path.node));
+ }
+ }
+ },
+ },
+ BlockCallExpression: {
+ exit(path) {
+ const node = path.node;
+ const callExpression = t.callExpression(
+ t.memberExpression(
+ t.identifier('this'),
+ t.identifier(toBlockName(node.callee.name)),
+ true,
+ ),
+ [t.identifier(path.scope.contextName)],
+ );
+ path.replaceWith(new Fragment(callExpression));
+ },
+ },
+ BlockStatement: {
+ exit(path) {
+ if (path.parentPath.is('EmbedStatement')) {
+ // todo implement
+ } else {
+ const node = path.node,
+ blockName = toBlockName(node.name.name);
+
+ const blockScope = path.scope;
+ if (blockScope.hasBinding('parent')) {
+ const bindings = blockScope.getBinding('parent');
+ for (const ref of (bindings.referencePaths: Array)) {
+ ref.replaceWithJS(
+ t.memberExpression(
+ t.identifier(this.parentName),
+ t.identifier(blockName),
+ ),
+ );
+ }
+ }
+ if (
+ this.template.parentName &&
+ !path.findParentPathOfType('BlockStatement')
+ ) {
+ // if we're in an inherited template and are not defined in
+ // another block statement
+ path.remove();
+ } else {
+ const callExpression = t.callExpression(
+ t.memberExpression(
+ t.identifier('this'),
+ t.identifier(blockName),
+ ),
+ [t.identifier(path.parentPath.scope.contextName)],
+ );
+ path.replaceWith(new Fragment(callExpression));
+ }
+ this.program.body.push(
+ buildRenderFunction({
+ TEMPLATE: t.identifier(this.templateVariableName),
+ NAME: t.identifier(blockName),
+ BODY: node.body,
+ }),
+ );
+ }
+ },
+ },
+ IncludeStatement: {
+ exit(path) {
+ const includeName = this.addDefaultImportFrom(
+ path.node.source.value,
+ path.scope.generateUid('include'),
+ );
+ path.scope.getRootScope().registerBinding(includeName);
+
+ const node = path.node;
+ let argument;
+
+ if (node.argument) {
+ if (node.contextFree) {
+ argument = node.argument;
+ } else {
+ argument = t.callExpression(
+ t.identifier(
+ this.addImportFrom(
+ 'melody-runtime',
+ 'createSubContext',
+ ),
+ ),
+ [t.identifier(path.scope.contextName), node.argument],
+ );
+ }
+ } else if (!node.contextFree) {
+ argument = t.identifier(path.scope.contextName);
+ }
+
+ const includeCall = t.callExpression(
+ t.identifier(includeName),
+ argument ? [argument] : [],
+ );
+ path.replaceWith(new Fragment(includeCall));
+ },
+ },
+ EmbedStatement: {
+ exit(path) {
+ // todo: if template has parent, check that we're in a block
+ const rootScope = path.scope.getRootScope();
+ const embedName = rootScope.generateUid('embed');
+ const importDecl = t.importDeclaration(
+ [
+ t.importSpecifier(
+ t.identifier(embedName),
+ t.identifier('_template'),
+ ),
+ ],
+ path.node.parent,
+ );
+ this.program.body.splice(0, 0, importDecl);
+ rootScope.registerBinding(embedName);
+
+ const embeddedName = rootScope.generateUid('embed');
+ rootScope.registerBinding(embeddedName);
+ let lastStmt = this.insertGlobalVariableDeclaration(
+ 'const',
+ t.identifier(embeddedName),
+ t.callExpression(
+ t.memberExpression(
+ t.identifier('Object'),
+ t.identifier('create'),
+ ),
+ [t.identifier(embedName)],
+ ),
+ );
+ if (path.get('blocks')) {
+ for (const blockPath of (path.get('blocks'): Array)) {
+ const block = blockPath.node;
+ const blockName =
+ 'render' +
+ block.name.name[0].toUpperCase() +
+ block.name.name.substring(1);
+ const stmt = t.expressionStatement(
+ t.assignmentExpression(
+ '=',
+ t.memberExpression(
+ t.identifier(embeddedName),
+ t.identifier(blockName),
+ ),
+ {
+ type: 'FunctionExpression',
+ id: null,
+ generator: false,
+ expression: false,
+ params: [t.identifier('_context')],
+ body: t.blockStatement(block.body),
+ },
+ ),
+ );
+ lastStmt = this.insertAfter(stmt, lastStmt);
+
+ const blockScope = blockPath.scope;
+ if (blockScope.hasBinding('parent')) {
+ const bindings = blockScope.getBinding('parent');
+ for (const ref of (bindings.referencePaths: Array)) {
+ ref.replaceWithJS(
+ t.memberExpression(
+ t.identifier(embedName),
+ t.identifier(blockName),
+ ),
+ );
+ }
+ }
+ }
+ }
+ let context = t.identifier(path.scope.contextName);
+ if (path.node.argument) {
+ if (path.node.contextFree) {
+ context = t.callExpression(
+ t.identifier(
+ this.addImportFrom(
+ 'melody-runtime',
+ 'createSubContext',
+ ),
+ ),
+ [context, path.node.argument],
+ );
+ } else {
+ context = t.callExpression(
+ t.identifier(
+ this.addImportFrom(
+ 'melody-runtime',
+ 'createSubContext',
+ ),
+ ),
+ [context, path.node.argument],
+ );
+ }
+ }
+
+ const callExpression = t.callExpression(
+ t.memberExpression(
+ t.identifier(embeddedName),
+ t.identifier('render'),
+ ),
+ [context],
+ );
+
+ path.replaceWith(new Fragment(callExpression));
+ },
+ },
+};
+
+function toBlockName(name) {
+ return 'render' + name[0].toUpperCase() + name.substring(1);
+}
diff --git a/packages/melody-compiler/src/convert/expressions.js b/packages/melody-compiler/src/convert/expressions.js
new file mode 100644
index 0000000..f301402
--- /dev/null
+++ b/packages/melody-compiler/src/convert/expressions.js
@@ -0,0 +1,338 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Fragment } from 'melody-types';
+import * as t from 'babel-types';
+import { isString, isFunction } from 'lodash';
+
+var operatorMap = {
+ or: '||',
+ and: '&&',
+ 'b-or': '|',
+ 'b-xor': '^',
+ 'b-and': '&',
+};
+
+export default {
+ UnaryNotExpression: {
+ exit(path) {
+ path.replaceWithJS(t.unaryExpression('!', path.node.argument));
+ },
+ },
+ UnaryExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.unaryExpression(
+ path.node.operator,
+ path.get('argument').node,
+ ),
+ );
+ },
+ },
+ BinaryConcatExpression: {
+ exit(path) {
+ const node = path.node;
+ path.replaceWithJS({
+ type: 'BinaryExpression',
+ operator: '+',
+ left: node.left,
+ right: node.right,
+ });
+ },
+ },
+ BinaryPowerExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.memberExpression(
+ t.identifier('Math'),
+ t.identifier('pow'),
+ ),
+ [path.get('left').node, path.get('right').node],
+ ),
+ );
+ },
+ },
+ BinaryNullCoalesceExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.conditionalExpression(
+ t.binaryExpression(
+ '!=',
+ path.get('left').node,
+ t.nullLiteral(),
+ ),
+ path.get('left').node,
+ path.get('right').node,
+ ),
+ );
+ },
+ },
+ BinaryFloorDivExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.identifier(this.addImportFrom('melody-runtime', 'round')),
+ [
+ t.binaryExpression(
+ '/',
+ path.get('left').node,
+ path.get('right').node,
+ ),
+ t.numericLiteral(0),
+ t.stringLiteral('floor'),
+ ],
+ ),
+ );
+ },
+ },
+ BinaryNotInExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.unaryExpression(
+ '!',
+ t.callExpression(
+ t.identifier(this.addImportFrom('lodash', 'includes')),
+ [path.get('right').node, path.get('left').node],
+ ),
+ ),
+ );
+ },
+ },
+ BinaryInExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.identifier(this.addImportFrom('lodash', 'includes')),
+ [path.get('right').node, path.get('left').node],
+ ),
+ );
+ },
+ },
+ BinaryStartsWithExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.identifier(this.addImportFrom('lodash', 'startsWith')),
+ [path.get('left').node, path.get('right').node],
+ ),
+ );
+ },
+ },
+ BinaryEndsWithExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.identifier(this.addImportFrom('lodash', 'endsWith')),
+ [path.get('left').node, path.get('right').node],
+ ),
+ );
+ },
+ },
+ BinaryRangeExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.identifier(this.addImportFrom('lodash', 'range')),
+ [path.get('left').node, path.get('right').node],
+ ),
+ );
+ },
+ },
+ BinaryMatchesExpression: {
+ exit(path) {
+ const right = path.get('right'),
+ pattern = right.is('StringLiteral')
+ ? t.regExpLiteral(right.node.value)
+ : right.node;
+ path.replaceWithJS(
+ t.unaryExpression(
+ '!',
+ t.unaryExpression(
+ '!',
+ t.callExpression(
+ t.memberExpression(
+ path.get('left').node,
+ t.identifier('match'),
+ ),
+ [pattern],
+ ),
+ ),
+ ),
+ );
+ },
+ },
+ BinaryExpression: {
+ exit(path) {
+ const node = path.node;
+ path.replaceWithJS({
+ type: 'BinaryExpression',
+ operator: operatorMap[node.operator] || node.operator,
+ left: node.left,
+ right: node.right,
+ });
+ },
+ },
+ CallExpression: {
+ exit(path) {
+ const callee = path.get('callee');
+ if (callee.is('Identifier')) {
+ const functionName = callee.node.name,
+ binding = callee.scope.getBinding(functionName);
+ if (binding) {
+ if (
+ binding.kind === 'macro' &&
+ path.parentPath.is('PrintStatement')
+ ) {
+ path.parentPath.replaceWith(
+ new Fragment(
+ t.callExpression(
+ t.identifier(functionName),
+ path.node.arguments,
+ ),
+ ),
+ );
+ } else if (binding.kind === 'function') {
+ const functionSource = this.functionMap[functionName];
+ if (isString(functionSource)) {
+ callee.node.name = this.addImportFrom(
+ functionSource,
+ functionName,
+ );
+ } else if (isFunction(functionSource)) {
+ functionSource(path);
+ }
+ }
+ }
+ }
+ },
+ },
+ ConditionalExpression: {
+ exit(path) {
+ const node = path.node;
+ if (!node.alternate) {
+ node.alternate = t.stringLiteral('');
+ } else if (!node.consequent) {
+ path.replaceWithJS({
+ type: 'LogicalExpression',
+ operator: '||',
+ left: node.test,
+ right: node.alternate,
+ });
+ }
+ },
+ },
+ SequenceExpression: {
+ exit(path) {
+ path.replaceWithJS({
+ type: 'BlockStatement',
+ body: path.node.expressions,
+ });
+ },
+ },
+ DoStatement: {
+ exit(path) {
+ path.replaceWithJS(t.expressionStatement(path.node.value));
+ },
+ },
+ BinaryAndExpression: {
+ exit(path) {
+ path.node.operator = '&&';
+ },
+ },
+ BinaryOrExpression: {
+ exit(path) {
+ path.node.operator = '||';
+ },
+ },
+ FilterExpression: {
+ exit(path) {
+ const expr = path.node,
+ filterSource = this.filterMap[expr.name.name];
+ if (!filterSource) {
+ return;
+ }
+ if (isString(filterSource)) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.identifier(
+ this.addImportFrom(filterSource, expr.name.name),
+ ),
+ [expr.target, ...expr.arguments],
+ ),
+ );
+ } else if (isFunction(filterSource)) {
+ filterSource.call(this, path);
+ }
+ },
+ },
+ IfStatement: {
+ exit(path) {
+ path.node.consequent = t.blockStatement(path.node.consequent);
+ if (path.node.alternate && Array.isArray(path.node.alternate)) {
+ path.node.alternate = t.blockStatement(path.node.alternate);
+ }
+ },
+ },
+ VariableDeclarationStatement: {
+ exit(path) {
+ const node = path.node;
+ const binding = path.scope.getBinding(node.name.name);
+ if (
+ path.scope.escapesContext ||
+ (binding && binding.shadowedBinding)
+ ) {
+ path.replaceWithJS(
+ t.assignmentExpression(
+ '=',
+ t.memberExpression(
+ t.identifier(path.scope.contextName),
+ node.name,
+ ),
+ node.value,
+ ),
+ );
+ } else {
+ path.replaceWithJS(
+ t.assignmentExpression('=', node.name, node.value),
+ );
+ }
+ },
+ },
+ SetStatement: {
+ exit(path) {
+ const assignments = [];
+ const replacements = [];
+ for (const expr of (path.node.assignments: Array)) {
+ if (t.isAssignmentExpression) {
+ assignments.push(expr);
+ } else {
+ // todo better error handling
+ throw new Error(
+ 'Must be variable declaration or assignment',
+ );
+ }
+ }
+ if (assignments.length) {
+ replacements.push(
+ ...path.node.assignments.map(expr =>
+ t.expressionStatement(expr),
+ ),
+ );
+ }
+ path.replaceWithMultipleJS(...replacements);
+ },
+ },
+};
diff --git a/packages/melody-compiler/src/convert/index.js b/packages/melody-compiler/src/convert/index.js
new file mode 100644
index 0000000..8e5703b
--- /dev/null
+++ b/packages/melody-compiler/src/convert/index.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { merge } from 'melody-traverse';
+import blockConverter from './blocks';
+import expressionConverter from './expressions';
+import templateConverter from './template';
+
+export default merge(templateConverter, expressionConverter, blockConverter);
diff --git a/packages/melody-compiler/src/convert/template.js b/packages/melody-compiler/src/convert/template.js
new file mode 100644
index 0000000..595a57f
--- /dev/null
+++ b/packages/melody-compiler/src/convert/template.js
@@ -0,0 +1,385 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+import { Fragment } from 'melody-types';
+import { parse } from 'path';
+import { camelCase } from 'lodash';
+
+// Not using lodash capitalize here, because it makes the rest of the string lowercase
+function capitalize(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+function getDisplayName(str) {
+ return capitalize(camelCase(str));
+}
+
+function buildRenderFunction(ctx) {
+ return t.expressionStatement(
+ t.assignmentExpression(
+ '=',
+ t.memberExpression(ctx.TEMPLATE, ctx.NAME),
+ t.functionExpression(
+ null,
+ [t.identifier('_context')],
+ t.blockStatement(ctx.BODY),
+ ),
+ ),
+ );
+}
+
+export default {
+ Identifier: {
+ exit(path) {
+ if (!this.isReferenceIdentifier(path)) {
+ return;
+ }
+ let binding = path.scope.getBinding(path.node.name),
+ contextName;
+ if (binding) {
+ const originalName = binding.getData('Identifier.OriginalName');
+ if (originalName) {
+ path.node.name = originalName;
+ }
+ if (binding.getData('ImportDeclaration.ImportFromSelf')) {
+ path.parentPath.replaceWith(path.parent.property);
+ } else if (binding.kind === 'context') {
+ contextName =
+ path.getData('Identifier.contextName') ||
+ binding.scope.contextName ||
+ path.scope.contextName;
+ } else if (
+ binding.kind === 'var' &&
+ binding.scope.escapesContext &&
+ binding.contextual
+ ) {
+ if (binding.shadowedBinding) {
+ binding = binding.getRootDefinition();
+ }
+ contextName =
+ path.getData('Identifier.contextName') ||
+ binding.scope.contextName ||
+ path.scope.contextName;
+ }
+ }
+ if (contextName) {
+ const replacement = t.memberExpression(
+ t.identifier(contextName),
+ t.identifier(path.node.name),
+ );
+ path.replaceWithJS(replacement);
+ }
+ },
+ },
+ PrintStatement: {
+ enter(path) {
+ // remove empty print statements
+ // todo should we really do this?
+ const value = path.get('value');
+ if (value.is('StringLiteral')) {
+ // if(value.node.value.trim() === '') {
+ if (value.node.value.match(/^\s*$/)) {
+ path.remove();
+ return;
+ } else if (
+ this.isInSpaceless() &&
+ Array.isArray(path.container) &&
+ Number.isInteger(path.key)
+ ) {
+ if (path.key === 0) {
+ value.node.value = value.node.value.replace(/^\s+/, '');
+ }
+ if (path.key === path.container.length - 1) {
+ value.node.value = value.node.value.replace(/\s+$/, '');
+ }
+ }
+ value.node.value = value.node.value.replace(
+ /^\s{2,}|\s{2,}$/g,
+ ' ',
+ );
+ }
+ },
+ },
+ SpacelessBlock: {
+ enter() {
+ this.enterSpaceless();
+ },
+ exit(path) {
+ this.exitSpaceless();
+ path.replaceWithMultipleJS(...path.node.body);
+ },
+ },
+ RootScope: {
+ enter(path) {
+ // handle "import _self as t;"
+ const scope = path.scope.getRootScope();
+ let localMacroImportUsages;
+ for (const identifier of Object.keys(
+ path.scope.getRootScope().bindings,
+ )) {
+ const binding = scope.getBinding(identifier);
+ if (
+ binding &&
+ binding.kind === 'macro' &&
+ binding.getData('ImportDeclaration.ImportFromSelf')
+ ) {
+ localMacroImportUsages = binding;
+ break;
+ }
+ }
+ if (localMacroImportUsages && localMacroImportUsages.referenced) {
+ for (const usage of (localMacroImportUsages.referencePaths: Array)) {
+ if (usage.parentPath.is('MemberExpression')) {
+ usage.parentPath.replaceWith(usage.parent.property);
+ }
+ }
+ }
+ },
+ exit(path) {
+ if (path.scope.mutated && path.scope.escapesContext) {
+ path.node.body.unshift(
+ t.variableDeclaration('const', [
+ t.variableDeclarator(
+ t.identifier(path.scope.contextName),
+ t.callExpression(
+ t.identifier(
+ this.addImportFrom(
+ 'melody-runtime',
+ 'createSubContext',
+ ),
+ ),
+ [t.identifier('_context')],
+ ),
+ ),
+ ]),
+ );
+ }
+ },
+ },
+ Scope: {
+ exit(path) {
+ insertVariableDeclarations(path);
+ },
+ },
+ Template: {
+ enter(path) {
+ // create general structure of template
+ this.templateVariableName = '_template';
+ path.scope.registerBinding(this.templateVariableName);
+ let fileName = this.file.fileName || '
';
+ const nameParts = parse(fileName);
+
+ const name = nameParts.name.split('.')[0];
+
+ if (name === 'index' || name === 'base') {
+ fileName = getDisplayName(parse(nameParts.dir).name);
+ } else {
+ fileName = getDisplayName(name);
+ }
+ this.fileName = this.generateUid(fileName);
+ this.markIdentifier(this.fileName);
+
+ if (path.node.parentName) {
+ this.parentName = path.scope.generateUid('parent');
+ path.scope.registerBinding(this.parentName);
+ this.program.body.splice(
+ 0,
+ 0,
+ t.exportNamedDeclaration(
+ t.variableDeclaration('const', [
+ t.variableDeclarator(
+ t.identifier(this.templateVariableName),
+ t.callExpression(
+ t.memberExpression(
+ t.identifier('Object'),
+ t.identifier('create'),
+ ),
+ [t.identifier(this.parentName)],
+ ),
+ ),
+ ]),
+ [],
+ ),
+ );
+ if (path.node.body.length) {
+ path.node.body.push(
+ new Fragment(
+ t.callExpression(
+ t.memberExpression(
+ t.memberExpression(
+ t.identifier(this.parentName),
+ t.identifier('render'),
+ ),
+ t.identifier('call'),
+ ),
+ [
+ t.identifier(this.templateVariableName),
+ t.identifier(path.scope.contextName),
+ ],
+ ),
+ ),
+ );
+ }
+ } else {
+ this.program.body.splice(
+ 0,
+ 0,
+ t.exportNamedDeclaration(
+ t.variableDeclaration('const', [
+ t.variableDeclarator(
+ t.identifier(this.templateVariableName),
+ t.objectExpression([]),
+ ),
+ ]),
+ [],
+ ),
+ );
+ }
+ path.replaceWith(path.node);
+ },
+ exit(path) {
+ if (path.scope.mutated && path.scope.escapesContext) {
+ path.node.body.unshift(
+ t.variableDeclaration('const', [
+ t.variableDeclarator(
+ t.identifier(path.scope.contextName),
+ t.callExpression(
+ t.identifier(
+ this.addImportFrom(
+ 'melody-runtime',
+ 'createSubContext',
+ ),
+ ),
+ [t.identifier('_context')],
+ ),
+ ),
+ ]),
+ );
+ }
+ insertVariableDeclarations(path);
+ const fileName = this.fileName;
+
+ if (!path.node.parentName) {
+ this.program.body.push(
+ buildRenderFunction({
+ TEMPLATE: t.identifier(this.templateVariableName),
+ NAME: t.identifier('render'),
+ BODY: path.node.body,
+ }),
+ );
+ } else {
+ const parentName = this.parentName;
+ const importDecl = t.importDeclaration(
+ [
+ t.importSpecifier(
+ t.identifier(parentName),
+ t.identifier('_template'),
+ ),
+ ],
+ path.node.parentName,
+ );
+ this.program.body.splice(0, 0, importDecl);
+ const body = path.get('body').map(e => e.node);
+ if (body.length) {
+ const renderFunction = buildRenderFunction({
+ TEMPLATE: t.identifier(this.templateVariableName),
+ NAME: t.identifier('render'),
+ BODY: body,
+ });
+ this.program.body.push(renderFunction);
+ }
+ }
+ this.program.body.push(
+ t.ifStatement(
+ t.binaryExpression(
+ '!==',
+ t.memberExpression(
+ t.memberExpression(
+ t.identifier('process'),
+ t.identifier('env'),
+ ),
+ t.identifier('NODE_ENV'),
+ ),
+ t.stringLiteral('production'),
+ ),
+ t.blockStatement([
+ t.expressionStatement(
+ t.assignmentExpression(
+ '=',
+ t.memberExpression(
+ t.identifier(this.templateVariableName),
+ t.identifier('displayName'),
+ ),
+ t.stringLiteral(fileName),
+ ),
+ ),
+ ]),
+ ),
+ );
+ this.program.body.push(
+ t.exportDefaultDeclaration(
+ t.functionDeclaration(
+ t.identifier(fileName),
+ [t.identifier('props')],
+ t.blockStatement([
+ t.returnStatement(
+ t.callExpression(
+ t.memberExpression(
+ t.identifier(this.templateVariableName),
+ t.identifier('render'),
+ ),
+ [t.identifier('props')],
+ ),
+ ),
+ ]),
+ ),
+ //t.identifier(this.templateVariableName),
+ ),
+ );
+ path.replaceWithJS(this.program);
+ },
+ },
+};
+
+function insertVariableDeclarations(path) {
+ const bindings = path.scope.bindings,
+ varDeclarations = [];
+ for (const name in bindings) {
+ const binding = bindings[name];
+ if (
+ binding &&
+ binding.contextual &&
+ !binding.scope.escapesContext &&
+ !binding.shadowedBinding
+ ) {
+ varDeclarations.push(t.variableDeclarator(t.identifier(name)));
+ }
+ }
+ if (varDeclarations.length) {
+ let body = path.node.body;
+ if (body && body.body) {
+ body = body.body;
+ }
+ if (!body) {
+ body = path.node.expressions;
+ }
+ if (!body) {
+ body = [];
+ }
+ //const body = (path.node.body && path.node.body.body) || path.node.expressions || [];
+ body.unshift(t.variableDeclaration('let', varDeclarations));
+ }
+}
diff --git a/packages/melody-compiler/src/finalizer/index.js b/packages/melody-compiler/src/finalizer/index.js
new file mode 100644
index 0000000..85f12eb
--- /dev/null
+++ b/packages/melody-compiler/src/finalizer/index.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import validate from './validate';
+
+export default validate;
diff --git a/packages/melody-compiler/src/finalizer/validate.js b/packages/melody-compiler/src/finalizer/validate.js
new file mode 100644
index 0000000..f566699
--- /dev/null
+++ b/packages/melody-compiler/src/finalizer/validate.js
@@ -0,0 +1,64 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+function throwMissingMelodyPluginError(path) {
+ this.error(
+ 'Missing melody plugin',
+ 0,
+ 'Cannot convert templates, since there seems to be no melody plugin to convert the template to a certain output format. You can choose for example between `melody-plugin-idom` and `melody-plugin-jsx`.',
+ );
+}
+
+export default {
+ Element: {
+ exit(path) {
+ throwMissingMelodyPluginError.call(this, path);
+ },
+ },
+ Fragment: {
+ exit(path) {
+ throwMissingMelodyPluginError.call(this, path);
+ },
+ },
+ PrintStatement: {
+ exit(path) {
+ throwMissingMelodyPluginError.call(this, path);
+ },
+ },
+ FilterExpression: {
+ exit(path) {
+ const expr = path.node;
+ const name = expr.name.name;
+
+ const validFilters = Object.keys(this.filterMap)
+ .sort()
+ .map(filter => `- ${filter}`)
+ .join(`\n `);
+
+ this.error(
+ `Unknown filter "${name}"`,
+ expr.name.loc.start,
+ `You've tried to invoke an unknown filter called "${name}".
+Some of the known filters include:
+
+ ${validFilters}
+
+Please report this as a bug if the filter you've tried to use is listed here:
+http://twig.sensiolabs.org/doc/filters/index.html`,
+ name.length,
+ );
+ },
+ },
+};
diff --git a/packages/melody-compiler/src/index.js b/packages/melody-compiler/src/index.js
new file mode 100644
index 0000000..27a63f0
--- /dev/null
+++ b/packages/melody-compiler/src/index.js
@@ -0,0 +1,126 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { CharStream, Lexer, TokenStream, Parser } from 'melody-parser';
+import { traverse, merge, Path, Scope } from 'melody-traverse';
+
+import { Template, File } from './Template.js';
+import analyse from './analyse/index.js';
+import convert from './convert/index.js';
+import State from './state/State.js';
+import finalizer from './finalizer/index.js';
+
+import * as t from 'babel-types';
+import generate from 'babel-generator';
+
+// workaround for https://github.com/rollup/rollup/issues/430
+import { TokenTypes } from 'melody-parser';
+export { Template, File, TokenTypes };
+
+export function compile(fileName: String, source: String, ...extensions) {
+ const root = parseString(fileName, source, ...extensions);
+ const template = new Template(root.expressions),
+ file = new File(fileName, template),
+ state = new State(file, source);
+ let analyseVisitor = analyse,
+ convertVisitor = convert;
+ for (const ext of extensions) {
+ if (ext.visitors) {
+ if (Array.isArray(ext.visitors)) {
+ for (const visitor of (ext.visitors: Array)) {
+ if (visitor.analyse) {
+ analyseVisitor = merge(analyseVisitor, visitor.analyse);
+ }
+ if (visitor.convert) {
+ convertVisitor = merge(convertVisitor, visitor.convert);
+ }
+ }
+ } else {
+ if (ext.visitors.analyse) {
+ analyseVisitor = merge(
+ analyseVisitor,
+ ext.visitors.analyse,
+ );
+ }
+ if (ext.visitors.convert) {
+ convertVisitor = merge(
+ convertVisitor,
+ ext.visitors.convert,
+ );
+ }
+ }
+ }
+ if (ext.filterMap) {
+ Object.assign(state.filterMap, ext.filterMap);
+ }
+ if (ext.functionMap) {
+ Object.assign(state.functionMap, ext.functionMap);
+ }
+ if (ext.options) {
+ Object.assign(state.options, ext.options);
+ }
+ }
+ convertVisitor = merge(convertVisitor, finalizer);
+ const scope = Scope.get(
+ Path.get({
+ container: file,
+ key: 'template',
+ }),
+ );
+ traverse(file, analyseVisitor, scope, state);
+ traverse(file, convertVisitor, scope, state);
+ return t.file(file.template);
+}
+
+function parseString(fileName: string, source: string, ...extensions) {
+ const lexer = new Lexer(new CharStream(source));
+ for (const ext of extensions) {
+ if (ext.unaryOperators) {
+ lexer.addOperators(...ext.unaryOperators.map(op => op.text));
+ }
+ if (ext.binaryOperators) {
+ lexer.addOperators(...ext.binaryOperators.map(op => op.text));
+ }
+ }
+ const parser = new Parser(new TokenStream(lexer));
+ for (const extension of extensions) {
+ if (extension.tags) {
+ for (const tag of (extension.tags: Array)) {
+ parser.addTag(tag);
+ }
+ }
+ if (extension.unaryOperators) {
+ for (const op of (extension.unaryOperators: Array)) {
+ parser.addUnaryOperator(op);
+ }
+ }
+ if (extension.binaryOperators) {
+ for (const op of (extension.binaryOperators: Array)) {
+ parser.addBinaryOperator(op);
+ }
+ }
+ if (extension.tests) {
+ for (const test of (extension.tests: Array)) {
+ parser.addTest(test);
+ }
+ }
+ }
+
+ return parser.parse();
+}
+
+export function toString(jsAst, code) {
+ return generate(jsAst, null, code);
+}
diff --git a/packages/melody-compiler/src/state/State.js b/packages/melody-compiler/src/state/State.js
new file mode 100644
index 0000000..914b04a
--- /dev/null
+++ b/packages/melody-compiler/src/state/State.js
@@ -0,0 +1,390 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import type { File } from '../Template';
+import codeFrame from 'melody-code-frame';
+import * as t from 'babel-types';
+import { relative } from 'path';
+import * as random from 'random-seed';
+
+export default class State {
+ constructor(file: File, source: String) {
+ this.file = file;
+ this.source = source;
+ this.template = file.template;
+ this.options = {
+ generateKey: true,
+ projectRoot: undefined,
+ };
+ this.program = {
+ type: 'Program',
+ body: [],
+ sourceType: 'module',
+ };
+ this._importCache = Object.create(null);
+ this.filterMap = Object.create(null);
+ this.functionMap = Object.create(null);
+ this._usedIdentifiers = Object.create(null);
+ this._spacelessStack = [];
+ }
+
+ generateKey() {
+ if (this.keyGenerator === undefined) {
+ this.keyGenerator = createKeyGenerator(
+ this.file.fileName,
+ this.options.projectRoot || process.cwd(),
+ );
+ }
+ return this.keyGenerator.generate();
+ }
+
+ enterSpaceless() {
+ this._spacelessStack.push(true);
+ }
+
+ exitSpaceless() {
+ this._spacelessStack.pop();
+ }
+
+ isInSpaceless() {
+ return !!this._spacelessStack[this._spacelessStack.length - 1];
+ }
+
+ error(message, pos, advice, length = 1) {
+ let errorMessage = `${message}\n`;
+ errorMessage += codeFrame({
+ rawLines: this.source,
+ lineNumber: pos.line,
+ colNumber: pos.column,
+ length,
+ });
+ if (advice) {
+ errorMessage += '\n\n' + advice;
+ }
+ throw new Error(errorMessage);
+ }
+
+ markIdentifier(name) {
+ this._usedIdentifiers[name] = true;
+ }
+
+ generateUid(nameHint: string = 'temp') {
+ const name = toIdentifier(nameHint);
+
+ let uid;
+ let i = 0;
+ do {
+ uid = generateUid(name, i);
+ i++;
+ } while (this._usedIdentifiers[uid]);
+
+ return uid;
+ }
+
+ generateComponentUid(nameHint: string = 'temp') {
+ const name = toIdentifier(nameHint);
+
+ let uid;
+ let i = 0;
+ do {
+ uid = generateComponentUid(name, i);
+ i++;
+ } while (this._usedIdentifiers[uid]);
+
+ return uid;
+ }
+
+ getImportFrom(source) {
+ if (this._importCache[source]) {
+ return this._importCache[source];
+ }
+
+ const body = this.program.body;
+ let i = 0;
+ for (const len = body.length; i < len; i++) {
+ const stmt = body[i];
+ if (stmt.type === 'ImportDeclaration') {
+ if (stmt.source.value === source) {
+ this._importCache[source] = stmt;
+ return stmt;
+ }
+ }
+ }
+ return null;
+ }
+
+ addNamespaceImportFrom(source, alias) {
+ const body = this.program.body;
+ let i = 0;
+ for (const len = body.length; i < len; i++) {
+ const stmt = body[i];
+ if (stmt.type === 'ImportDeclaration') {
+ if (stmt.source.value === source) {
+ if (stmt.specifiers.length === 1) {
+ const specifier = stmt.specifiers[0];
+ if (
+ specifier.type === 'ImportNamespaceSpecifier' &&
+ specifier.local.name === alias
+ ) {
+ return stmt;
+ }
+ }
+ }
+ }
+ }
+ const importDeclaration = t.importDeclaration(
+ [t.importNamespaceSpecifier(t.identifier(alias))],
+ t.stringLiteral(source),
+ );
+ this.program.body.splice(0, 0, importDeclaration);
+ return importDeclaration;
+ }
+
+ addImportFrom(source, identifier, local = this.generateUid(identifier)) {
+ let importDecl = this.getImportFrom(source);
+ if (importDecl) {
+ let i = 0;
+ let isNamespaceImport = false;
+ for (
+ const specs = importDecl.specifiers, len = specs.length;
+ i < len;
+ i++
+ ) {
+ const spec = specs[i];
+ if (
+ spec.type === 'ImportSpecifier' &&
+ spec.imported &&
+ spec.imported.name === identifier
+ ) {
+ // already imported it
+ return spec.local.name;
+ } else if (spec.type === 'ImportNamespaceSpecifier') {
+ isNamespaceImport = true;
+ break;
+ }
+ }
+
+ if (!isNamespaceImport) {
+ importDecl.specifiers.push(
+ t.importSpecifier(
+ t.identifier(local),
+ t.identifier(identifier),
+ ),
+ );
+ return local;
+ }
+ }
+
+ importDecl = t.importDeclaration(
+ [t.importSpecifier(t.identifier(local), t.identifier(identifier))],
+ t.stringLiteral(source),
+ );
+ this._importCache[source] = importDecl;
+ this.program.body.splice(0, 0, importDecl);
+ return local;
+ }
+
+ addDefaultImportFrom(source, local = this.generateUid()) {
+ let importDecl = this.getImportFrom(source);
+ if (!importDecl) {
+ importDecl = t.importDeclaration(
+ [t.importDefaultSpecifier(t.identifier(local))],
+ t.stringLiteral(source),
+ );
+ this._importCache[source] = importDecl;
+ this.program.body.splice(0, 0, importDecl);
+ } else {
+ if (importDecl.specifiers[0].type === 'ImportDefaultSpecifier') {
+ return importDecl.specifiers[0].local.name;
+ }
+ importDecl.specifiers.unshift(
+ t.importDefaultSpecifier(t.identifier(local)),
+ );
+ }
+ return local;
+ }
+
+ insertGlobalVariableDeclaration(kind, id, init) {
+ const decl = t.variableDeclarator(id, init);
+ for (const stmt of (this.program.body: Array)) {
+ if (stmt.type === 'VariableDeclaration' && stmt.kind === kind) {
+ stmt.declarations.push(decl);
+ return stmt;
+ }
+ }
+ const stmt = t.variableDeclaration(kind, [decl]);
+ this.program.body.push(stmt);
+ return stmt;
+ }
+
+ insertAfter(stmt, sibling) {
+ const index = this.program.body.indexOf(sibling);
+ this.program.body.splice(index + 1, 0, stmt);
+ return stmt;
+ }
+
+ insertBefore(stmt, sibling) {
+ const index = this.program.body.indexOf(sibling);
+ this.program.body.splice(index, 0, stmt);
+ return stmt;
+ }
+
+ isReferenceIdentifier(path) {
+ const parentPath = path.parentPath,
+ key = path.parentKey;
+
+ if (
+ parentPath.is('MemberExpression') &&
+ key === 'property' &&
+ !parentPath.node.computed
+ ) {
+ return false;
+ }
+
+ if (parentPath.is('NamedArgumentExpression')) {
+ return false;
+ }
+
+ if (
+ parentPath.is('MountStatement') &&
+ key === 'name' &&
+ parentPath.node.source
+ ) {
+ return false;
+ }
+
+ if (
+ parentPath.is('ObjectProperty') &&
+ key === 'key' &&
+ !parentPath.node.computed
+ ) {
+ return false;
+ }
+
+ if (parentPath.is('Attribute') && key !== 'value') {
+ return false;
+ }
+
+ if (parentPath.is('BlockStatement') && key === 'name') {
+ return false;
+ }
+
+ if (
+ parentPath.is('ForStatement') &&
+ (key === 'keyTarget' || key === 'valueTarget')
+ ) {
+ return false;
+ }
+
+ if (
+ parentPath.is('ImportDeclaration') ||
+ parentPath.is('AliasExpression')
+ ) {
+ return false;
+ }
+
+ if (
+ parentPath.is('MacroDeclarationStatement') &&
+ (key === 'name' || key === 'arguments')
+ ) {
+ return false;
+ }
+
+ if (parentPath.is('VariableDeclarationStatement') && key === 'name') {
+ return false;
+ }
+
+ if (parentPath.is('CallExpression') && key === 'callee') {
+ return false;
+ }
+
+ return true;
+ }
+}
+
+function toIdentifier(nameHint) {
+ let name = nameHint + '';
+ name = name.replace(/[^a-zA-Z0-9$_]/g, '');
+
+ name = name.replace(/^[-0-9]+/, '');
+ name = name.replace(/[-\s]+(.)?/, function(match, c) {
+ return c ? c.toUpperCase() : '';
+ });
+
+ name = name.replace(/^_+/, '').replace(/[0-9]+$/, '');
+ return name;
+}
+
+function generateUid(name, i) {
+ if (i > 0) {
+ return `${name}$${i}`;
+ }
+ return name;
+}
+
+function capitalize(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+function generateComponentUid(name, i) {
+ const finalName = capitalize(name);
+ if (i > 0) {
+ return `${finalName}$${i}`;
+ }
+ return finalName;
+}
+
+/**
+ * If filename is not defined, generates random keys based on Math.random.
+ * Otherwise, uses filename as a seed for deterministic keys. Filename is
+ * converted to a relative path based on `projectRoot`.
+ */
+function createKeyGenerator(filename, projectRoot) {
+ // if filename is not defined, generate random keys
+ // based on Math.random
+ // otherwise, use filename as a seed for deterministic keys
+ const relativePath = filename ? relative(projectRoot, filename) : undefined;
+
+ const generator = random.create(relativePath);
+ return {
+ generate() {
+ let i;
+ let s = '';
+ // 7 chars long keys
+ for (i = 0; i < 7; i++) {
+ // start from 33th ASCII char (!), skipping quote ("), ampersand (&)
+ // and backslash (\)
+ // math: 33 + 91 + 3 = 127, 126 (tilde ~) is the last char we allow
+ // where 3 is number of chars we skip between 33-127
+
+ let rand = 33 + generator(91);
+ // skip quote (")
+ if (rand >= 34) {
+ rand++;
+ }
+ // skip ampersand (&)
+ if (rand >= 38) {
+ rand++;
+ }
+ // skip backslash (\)
+ if (rand >= 92) {
+ rand++;
+ }
+ s += String.fromCharCode(rand);
+ }
+ return s;
+ },
+ };
+}
diff --git a/packages/melody-component/__tests__/ComponentSpec.js b/packages/melody-component/__tests__/ComponentSpec.js
new file mode 100644
index 0000000..f66f264
--- /dev/null
+++ b/packages/melody-component/__tests__/ComponentSpec.js
@@ -0,0 +1,810 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { assert } from 'chai';
+
+import { createComponent, RECEIVE_PROPS, render } from '../src';
+import {
+ patch,
+ patchOuter,
+ flush,
+ component,
+ ref,
+ elementOpen,
+ elementClose,
+ text,
+} from 'melody-idom';
+
+describe('Component', function() {
+ it('should trigger componentDidMount once', function() {
+ const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ let mounted = 0;
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ mounted++;
+ },
+ });
+
+ render(root, MyComponent, { text: 'hello' });
+ assert.equal(root.outerHTML, 'hello
');
+ assert.equal(mounted, 1);
+
+ render(root, MyComponent, { text: 'test' });
+ assert.equal(root.outerHTML, 'test
');
+ assert.equal(mounted, 1);
+ });
+
+ it('mixins can be applied with a curried function', function() {
+ const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ let mounted = 0;
+ const MyComponent = createComponent(template)({
+ componentDidMount() {
+ mounted++;
+ },
+ });
+
+ render(root, MyComponent, { text: 'hello' });
+ assert.equal(root.outerHTML, 'hello
');
+ assert.equal(mounted, 1);
+
+ render(root, MyComponent, { text: 'test' });
+ assert.equal(root.outerHTML, 'test
');
+ assert.equal(mounted, 1);
+ });
+
+ it('mixins can be applied arbitrarily often with a curried function', function() {
+ const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ const mounted = [0, 0, 0];
+
+ const countMounting = idx => ({ componentDidMount }) => ({
+ componentDidMount() {
+ componentDidMount.call(this);
+ mounted[idx]++;
+ },
+ });
+
+ const MyComponent = createComponent(template)(countMounting(0))(
+ countMounting(1),
+ )(countMounting(2));
+
+ render(root, MyComponent, { text: 'hello' });
+ assert.equal(root.outerHTML, 'hello
');
+ assert.equal(mounted[0], 1);
+ assert.equal(mounted[1], 1);
+ assert.equal(mounted[2], 1);
+
+ render(root, MyComponent, { text: 'test' });
+ assert.equal(root.outerHTML, 'test
');
+ assert.equal(mounted[0], 1);
+ assert.equal(mounted[1], 1);
+ assert.equal(mounted[2], 1);
+ });
+
+ it('should trigger componentWillUnmount when a Component is removed', function() {
+ const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ elementOpen('p', null, null);
+ text(_context.text);
+ elementClose('p');
+ elementOpen('span');
+ text('foo');
+ elementClose('span');
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ let unmounted = 0;
+ const MyComponent = createComponent(template, undefined, {
+ componentWillUnmount() {
+ unmounted++;
+ },
+ });
+
+ const renderTemplate = _context => {
+ elementOpen('div');
+ if (_context.comp) {
+ component(MyComponent, 'test', { text: 'hello' });
+ }
+ elementClose('div');
+ };
+
+ patchOuter(root, renderTemplate, { comp: true });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ assert.equal(root.innerHTML, '');
+ assert.equal(unmounted, 0);
+
+ patchOuter(root, renderTemplate, { comp: false });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ assert.equal(root.innerHTML, '');
+ assert.equal(unmounted, 1);
+ });
+
+ it('should trigger componentWillUnmount when a Component is removed within an element', function() {
+ const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ let unmounted = 0;
+ const MyComponent = createComponent(template, undefined, {
+ componentWillUnmount() {
+ unmounted++;
+ },
+ });
+
+ const renderTemplate = _context => {
+ elementOpen('div');
+ if (_context.comp) {
+ elementOpen('div');
+ component(MyComponent, 'test', { text: 'hello' });
+ elementClose('div');
+ }
+ elementClose('div');
+ };
+
+ patchOuter(root, renderTemplate, { comp: true });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ assert.equal(root.innerHTML, '');
+ assert.equal(unmounted, 0);
+
+ patchOuter(root, renderTemplate, { comp: false });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ assert.equal(root.innerHTML, '');
+ assert.equal(unmounted, 1);
+ });
+
+ it('should trigger componentWillUnmount for child components when a Component is removed', function() {
+ let MyComponent;
+ const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ if (_context.comp) {
+ component(MyComponent, 'child', {
+ text: 'world',
+ comp: false,
+ });
+ }
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ let unmounted = 0;
+ MyComponent = createComponent(template)({
+ componentDidMount() {
+ unmounted++;
+ },
+ componentWillUnmount() {
+ unmounted--;
+ },
+ });
+
+ const renderTemplate = _context => {
+ elementOpen('div');
+ if (_context.comp) {
+ component(MyComponent, 'test', { text: 'hello', comp: true });
+ }
+ elementClose('div');
+ };
+
+ patchOuter(root, renderTemplate, { comp: true });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ assert.equal(root.innerHTML, '');
+ assert.equal(unmounted, 2);
+
+ patchOuter(root, renderTemplate, { comp: false });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ assert.equal(root.innerHTML, '');
+ assert.equal(unmounted, 0);
+ });
+
+ it('should trigger componentWillUnmount for deep nested child components when a Component is removed', function() {
+ let unmounted = { inner: 0, middle: 0, outer: 0 };
+ const root = document.createElement('div');
+ const CountInstances = {
+ componentDidMount() {
+ unmounted[this.name]++;
+ },
+ componentWillUnmount() {
+ unmounted[this.name]--;
+ },
+ };
+
+ const InnerComponent = createComponent({
+ render(_context) {
+ elementOpen('div', null, null);
+ elementClose('div');
+ },
+ })({ name: 'inner' }, CountInstances);
+
+ const MiddleComponent = createComponent({
+ render(_context) {
+ elementOpen('div', null, null);
+ component(InnerComponent, 'child', { inner: true });
+ elementClose('div');
+ },
+ })({ name: 'middle' }, CountInstances);
+
+ const OuterComponent = createComponent({
+ render(_context) {
+ elementOpen('div', null, null);
+ component(MiddleComponent, 'child', {});
+ elementClose('div');
+ },
+ })({ name: 'outer' }, CountInstances);
+
+ const renderTemplate = _context => {
+ elementOpen('div');
+ if (_context.comp) {
+ component(OuterComponent, 'test', {});
+ }
+ elementClose('div');
+ };
+
+ patchOuter(root, renderTemplate, { comp: true });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ assert.equal(root.innerHTML, '');
+ assert.equal(unmounted.inner, 1);
+ assert.equal(unmounted.middle, 1);
+ assert.equal(unmounted.outer, 1);
+
+ patchOuter(root, renderTemplate, { comp: false });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ assert.equal(root.innerHTML, '');
+ assert.equal(unmounted.inner, 0);
+ assert.equal(unmounted.middle, 0);
+ assert.equal(unmounted.outer, 0);
+ });
+
+ it('should trigger componentWillUnmount for deep nested child components when a Component is removed', function() {
+ let unmounted = { innermost: 0, inner: 0, middle: 0, outer: 0 };
+ const root = document.createElement('div');
+ const CountInstances = {
+ componentDidMount() {
+ unmounted[this.name]++;
+ },
+ componentWillUnmount() {
+ unmounted[this.name]--;
+ },
+ };
+
+ const InnerMostComponent = createComponent({
+ render(_context) {
+ elementOpen('div', null, null);
+ elementClose('div');
+ },
+ })({ name: 'innermost' }, CountInstances);
+
+ const InnerComponent = createComponent({
+ render(_context) {
+ elementOpen('div', null, null);
+ component(InnerMostComponent, 'child', {});
+ elementClose('div');
+ },
+ })({ name: 'inner' }, CountInstances);
+
+ const MiddleComponent = createComponent({
+ render(_context) {
+ elementOpen('div', null, null);
+ component(InnerComponent, 'child', {});
+ elementClose('div');
+ },
+ })({ name: 'middle' }, CountInstances);
+
+ const OuterComponent = createComponent({
+ render(_context) {
+ elementOpen('div', null, null);
+ if (_context.comp) {
+ component(MiddleComponent, 'child', {});
+ }
+ elementClose('div');
+ },
+ })({ name: 'outer' }, CountInstances);
+
+ render(root, OuterComponent, { comp: true });
+ assert.equal(root.innerHTML, '');
+ assert.equal(unmounted.inner, 1);
+ assert.equal(unmounted.middle, 1);
+ assert.equal(unmounted.outer, 1);
+
+ render(root, OuterComponent, { comp: false });
+ assert.equal(root.innerHTML, '');
+ assert.equal(unmounted.inner, 0);
+ assert.equal(unmounted.middle, 0);
+ assert.equal(unmounted.outer, 1);
+ });
+
+ it('should register refs', function() {
+ const statics = ['ref', ref('fun')];
+ const template = {
+ render(_context) {
+ elementOpen('div', null, statics);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ assert(this.refs.fun != null, 'fun ref should exist');
+ this.refs.fun.innerHTML = 'test';
+ },
+
+ componentDidUpdate(prevProps, prevState) {
+ expect(this.el.outerHTML).toEqual('world
');
+ this.refs.fun.innerHTML = 'fun!';
+ assert(prevState !== this.state);
+ expect(prevProps).not.toEqual(this.props);
+ assert(prevState.text === 'hello');
+ assert(prevProps.text === 'hello');
+ assert.equal(this.state.text, 'world');
+ assert.equal(this.props.text, 'world');
+ },
+ });
+
+ render(root, MyComponent, { text: 'hello' });
+ assert.equal(root.outerHTML, 'test
');
+
+ render(root, MyComponent, { text: 'world' });
+ assert.equal(root.outerHTML, 'fun!
');
+ });
+
+ it('should register refs for the current element', function() {
+ const statics = ['ref', ref('fun')];
+ const template = {
+ render(_context) {
+ elementOpen('div', null, statics);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ assert(this.refs.fun != null, 'fun ref should exist');
+ this.refs.fun.innerHTML = 'test';
+ },
+ });
+
+ render(root, MyComponent, { text: 'hello' });
+ assert.equal(root.outerHTML, 'test
');
+ });
+
+ it('should throw when registering a ref outside of a component', function() {
+ const root = document.createElement('div');
+ const statics = ['ref', ref('test')];
+ assert.throws(() => {
+ patch(root, () => {
+ elementOpen('div', null, statics);
+ text('test');
+ elementClose('div');
+ });
+ }, 'ref() must be used within a component');
+ });
+
+ it('should trigger componentDidMount once even for nested components', function() {
+ const childTemplate = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+
+ let mounted = 0;
+ const MyComponent = createComponent(childTemplate, undefined, {
+ componentDidMount() {
+ mounted++;
+ assert(this.el != null, 'Element should exists');
+ },
+ });
+
+ const parentTemplate = {
+ render(_context) {
+ elementOpen('div', null, null);
+ component(MyComponent, 'MyComponent', _context.childProps);
+ elementClose('div');
+ },
+ };
+ const MyParentComponent = createComponent(parentTemplate);
+
+ const root = document.createElement('div');
+ render(root, MyParentComponent, { childProps: { text: 'hello' } });
+ assert.equal(root.outerHTML, '');
+ assert.equal(mounted, 1);
+
+ render(root, MyParentComponent, { childProps: { text: 'test' } });
+ assert.equal(root.outerHTML, '');
+ assert.equal(mounted, 1);
+ });
+
+ it('should have an element during componentDidMount', function() {
+ const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ let mounted = 0;
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ mounted++;
+ assert.equal(this.el, root, 'Element should be set');
+ },
+ });
+
+ render(root, MyComponent, { text: 'hello' });
+ assert.equal(mounted, 1);
+ });
+
+ it('should not render if data is unchanged', function() {
+ const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ const MyComponent = createComponent(template);
+ const props = { text: 'hello' };
+ render(root, MyComponent, props);
+ assert.equal(root.innerHTML, 'hello');
+ props.text = 'world';
+ render(root, MyComponent, props);
+ assert.equal(root.innerHTML, 'hello');
+ });
+
+ it('should mount onto an element without a key', function() {
+ const template = {
+ render(_context) {
+ elementOpen('div', 'test', null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ const MyComponent = createComponent(template);
+
+ render(root, MyComponent, { text: 'hello' });
+ assert.equal(root.outerHTML, 'hello
');
+
+ render(root, MyComponent, { text: 'test' });
+ assert.equal(root.outerHTML, 'test
');
+ });
+
+ it('should replace components', function() {
+ const template = {
+ render(_context) {
+ elementOpen('div', 'test', null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ const MyComponent = createComponent(template);
+ const MyOtherComponent = createComponent(template);
+
+ render(root, MyComponent, { text: 'hello' });
+ assert.equal(root.outerHTML, 'hello
');
+
+ render(root, MyOtherComponent, { text: 'test' });
+ assert.equal(root.outerHTML, 'test
');
+ });
+
+ it('should unmount replaced components', function() {
+ const template = {
+ render(_context) {
+ elementOpen('div', 'test', null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ let unmounted = 0;
+ const MyComponent = createComponent(template, undefined, {
+ componentWillUnmount() {
+ unmounted++;
+ },
+ });
+ const MyOtherComponent = createComponent(template);
+
+ render(root, MyComponent, { text: 'hello' });
+ assert.equal(root.outerHTML, 'hello
');
+
+ render(root, MyOtherComponent, { text: 'test' });
+ assert.equal(root.outerHTML, 'test
');
+ assert.equal(unmounted, 1);
+ });
+
+ it('should render components into an existing DOM', function() {
+ const childTemplate = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+
+ let mounted = 0;
+ const MyComponent = createComponent(childTemplate, undefined, {
+ componentDidMount() {
+ mounted++;
+ if (mounted === 3) {
+ throw new Error('gotcha!');
+ }
+ },
+ });
+
+ const parentTemplate = {
+ render(_context) {
+ elementOpen('div', null, null);
+ component(MyComponent, '3', _context.childProps);
+ elementClose('div');
+ },
+ };
+ const MyParentComponent = createComponent(parentTemplate);
+
+ const root = document.createElement('div');
+ root.innerHTML = 'test
';
+ assert.equal(root.outerHTML, '');
+ render(root, MyParentComponent, { childProps: { text: 'hello' } });
+ assert.equal(root.outerHTML, '');
+ assert.equal(mounted, 1);
+ });
+
+ it('should render components into an existing DOM', function() {
+ const childTemplate = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+
+ let mounted = 0;
+ const MyComponent = createComponent(childTemplate, undefined, {
+ componentDidMount() {
+ mounted++;
+ if (mounted === 3) {
+ throw new Error('gotcha!');
+ }
+ },
+ });
+
+ const parentTemplate = {
+ render(_context) {
+ elementOpen('div', null, null);
+ component(MyComponent, '4', _context.childProps);
+ elementClose('div');
+ },
+ };
+ const MyParentComponent = createComponent(parentTemplate);
+
+ const root = document.createElement('div');
+ root.innerHTML = 'test
';
+ assert.equal(root.outerHTML, '');
+ const oldChild = root.children[0];
+ render(root, MyParentComponent, { childProps: { text: 'hello' } });
+ assert.equal(root.outerHTML, '');
+ assert.equal(mounted, 1);
+ assert.notEqual(oldChild, root.children[0]);
+ assert(
+ oldChild.parentNode == null,
+ 'Previous child no longer has a parent',
+ );
+ });
+
+ it('should reuse moved child components', function() {
+ const childTemplate = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+
+ let mounted = 0;
+ const MyComponent = createComponent(childTemplate, undefined, {
+ componentDidMount() {
+ mounted++;
+ if (mounted === 3) {
+ throw new Error('gotcha!');
+ }
+ },
+ });
+
+ const parentTemplate = {
+ render(_context) {
+ elementOpen('div', null, null);
+ if (_context.flip) {
+ component(MyComponent, '2', _context.childProps[1]);
+ component(MyComponent, '1', _context.childProps[0]);
+ } else {
+ component(MyComponent, '1', _context.childProps[0]);
+ component(MyComponent, '2', _context.childProps[1]);
+ }
+ elementClose('div');
+ },
+ };
+ const MyParentComponent = createComponent(parentTemplate);
+
+ const root = document.createElement('div');
+ render(root, MyParentComponent, {
+ childProps: [{ text: 'hello' }, { text: 'world' }],
+ });
+ const firstCompEl = root.childNodes[0];
+ const secondCompEl = root.childNodes[1];
+ assert.equal(
+ root.outerHTML,
+ '',
+ );
+ assert.equal(mounted, 2);
+
+ render(root, MyParentComponent, {
+ flip: true,
+ childProps: [{ text: 'hello' }, { text: 'world' }],
+ });
+ assert.equal(
+ root.outerHTML,
+ '',
+ );
+ assert.equal(firstCompEl, root.childNodes[1]);
+ assert.equal(secondCompEl, root.childNodes[0]);
+ assert.equal(mounted, 2);
+ });
+
+ it('should render existing components into an existing DOM', function() {
+ const childTemplate = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+
+ let mounted = 0;
+ const MyComponent = createComponent(childTemplate, undefined, {
+ componentDidMount() {
+ mounted++;
+ },
+ });
+
+ const parentTemplate = {
+ render(_context) {
+ elementOpen('div', null, null);
+ component(MyComponent, '4', _context.childProps);
+ elementClose('div');
+ },
+ };
+ const MyParentComponent = createComponent(parentTemplate);
+
+ const root = document.createElement('div');
+ root.innerHTML = 'test
';
+ assert.equal(root.outerHTML, '');
+ const oldChild = root.children[0];
+ render(root, MyParentComponent, { childProps: { text: 'hello' } });
+ assert.equal(root.outerHTML, '');
+ assert.equal(mounted, 1);
+ assert.notEqual(oldChild, root.children[0]);
+ assert(
+ oldChild.parentNode == null,
+ 'Previous child no longer has a parent',
+ );
+ });
+
+ it('should update itself when its state changes', function(done) {
+ const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text(_context.text);
+ elementClose('div');
+ },
+ };
+ const root = document.createElement('div');
+ let comp;
+ const MyComponent = createComponent(
+ template,
+ (state = { text: '' }, action) => {
+ if (action.type === 'setText') {
+ return { ...state, text: action.payload };
+ } else if (action.type === RECEIVE_PROPS) {
+ comp = action.meta;
+ return { ...state, ...action.payload };
+ }
+ return state;
+ },
+ );
+
+ render(root, MyComponent, { text: 'hello' });
+ assert.equal(root.outerHTML, 'hello
');
+ comp.dispatch({ type: 'setText', payload: 'world' });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ assert.equal(root.outerHTML, 'world
');
+ done();
+ });
+});
diff --git a/packages/melody-component/__tests__/EnsureKeySpec.js b/packages/melody-component/__tests__/EnsureKeySpec.js
new file mode 100644
index 0000000..0b323f4
--- /dev/null
+++ b/packages/melody-component/__tests__/EnsureKeySpec.js
@@ -0,0 +1,166 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createComponent, render, RECEIVE_PROPS } from '../src';
+import { elementOpen, elementClose, component, text, flush } from 'melody-idom';
+
+let root, parent, children, Parent, Child;
+
+const childTemplate = {
+ render(state) {
+ elementOpen('span');
+ text(`${state.paddedIndex} and ${state.childCount}`);
+ elementClose('span');
+ },
+};
+
+const parentTemplate = {
+ render(count) {
+ elementOpen('div', 'parent');
+ elementOpen('span');
+ text('Some element without key');
+ elementClose('span');
+ for (let i = 0; i < 5; i++) {
+ component(Child, `${i}`, {
+ paddedIndex: `${i + count}`,
+ });
+ }
+ elementClose('div');
+ },
+};
+
+function createDOM() {
+ parent = document.createElement('div');
+ parent.setAttribute('key', 'parent');
+ children = [];
+
+ const someChild = document.createElement('span');
+ someChild.textContent = 'Some element without key';
+ parent.appendChild(someChild);
+
+ for (let i = 0; i < 5; i++) {
+ const child = document.createElement('span');
+ child.setAttribute('key', `${i}`);
+ child.textContent = `${i}`;
+ parent.appendChild(child);
+ children.push(child);
+ }
+}
+
+beforeEach(() => {
+ Child = createComponent(
+ childTemplate,
+ (state = { childCount: 0 }, { type, payload }) => {
+ if (type === 'INC') {
+ return {
+ ...state,
+ childCount: state.childCount + 1,
+ };
+ }
+ if (type === RECEIVE_PROPS) {
+ return { ...state, ...payload };
+ }
+ return state;
+ },
+ );
+ Parent = createComponent(parentTemplate, (count = 0, { type }) => {
+ if (type === 'INC') {
+ return count + 1;
+ }
+ return count;
+ });
+ root = document.createElement('div');
+ createDOM();
+ root.appendChild(parent);
+ render(parent, Parent);
+});
+
+test('should mount on top of existing keyed components', () => {
+ expect(root.innerHTML).toMatchSnapshot();
+});
+
+test('parent should rerender', done => {
+ parent.__incrementalDOMData.componentInstance.dispatch({ type: 'INC' });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ expect(root.innerHTML).toMatchSnapshot();
+ done();
+});
+
+test('children should rerender', done => {
+ for (let i = 0; i < 5; i++) {
+ children[i].__incrementalDOMData.componentInstance.dispatch({
+ type: 'INC',
+ });
+ }
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ expect(root.innerHTML).toMatchSnapshot();
+ done();
+});
+
+test('parent should rerender after children rerender', done => {
+ for (let i = 0; i < 5; i++) {
+ children[i].__incrementalDOMData.componentInstance.dispatch({
+ type: 'INC',
+ });
+ }
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ parent.__incrementalDOMData.componentInstance.dispatch({ type: 'INC' });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ expect(root.innerHTML).toMatchSnapshot();
+ done();
+});
+
+test('children should rerender after parent rerender', done => {
+ parent.__incrementalDOMData.componentInstance.dispatch({ type: 'INC' });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ for (let i = 0; i < 5; i++) {
+ children[i].__incrementalDOMData.componentInstance.dispatch({
+ type: 'INC',
+ });
+ }
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ expect(root.innerHTML).toMatchSnapshot();
+ done();
+});
diff --git a/packages/melody-component/__tests__/MarkDirtySpec.js b/packages/melody-component/__tests__/MarkDirtySpec.js
new file mode 100644
index 0000000..2ca0533
--- /dev/null
+++ b/packages/melody-component/__tests__/MarkDirtySpec.js
@@ -0,0 +1,88 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { assert } from 'chai';
+
+import { createComponent, render, RECEIVE_PROPS } from '../src';
+import { elementOpen, elementClose, component, text } from 'melody-idom';
+
+const instanceAccessor = setter => {
+ return ({ componentWillMount }) => ({
+ componentWillMount() {
+ componentWillMount.call(this);
+ setter(this);
+ },
+ });
+};
+
+it('should not try to render an already unmounted child', done => {
+ const childTemplate = {
+ render() {
+ elementOpen('div');
+ text('foo');
+ elementClose('div');
+ },
+ };
+
+ let childInstance;
+ // always return new state to force rerender
+ const ChildComponent = createComponent(
+ childTemplate,
+ state => ({}),
+ instanceAccessor(i => (childInstance = i)),
+ );
+
+ const parentReducer = (state = { show: true }, action) => {
+ if (action.type === RECEIVE_PROPS) {
+ return {
+ ...state,
+ ...action.payload,
+ };
+ }
+ if (action.type === 'HIDE') {
+ return {
+ ...state,
+ show: false,
+ };
+ }
+ return state;
+ };
+
+ const parentTemplate = {
+ render(_context) {
+ elementOpen('div');
+ if (_context.show) {
+ component(ChildComponent, 'child');
+ }
+ elementClose('div');
+ },
+ };
+
+ let parentInstance;
+ const ParentComponent = createComponent(
+ parentTemplate,
+ parentReducer,
+ instanceAccessor(i => (parentInstance = i)),
+ );
+
+ const root = document.createElement('div');
+ render(root, ParentComponent);
+
+ // dispatch on parent instance first, which will delete child nodes
+ // then dispatch something to child to trigger rerender
+ parentInstance.dispatch({ type: 'HIDE' });
+ childInstance.dispatch({});
+ done();
+});
diff --git a/packages/melody-component/__tests__/UnmountSpec.js b/packages/melody-component/__tests__/UnmountSpec.js
new file mode 100644
index 0000000..8c39411
--- /dev/null
+++ b/packages/melody-component/__tests__/UnmountSpec.js
@@ -0,0 +1,78 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { assert } from 'chai';
+import { createComponent, render, unmountComponentAtNode } from '../src';
+import { component, elementOpen, elementClose, text } from 'melody-idom';
+
+describe('Unmount', function() {
+ describe('unmountComponentAtNode', function() {
+ it('should trigger componentWillUnmount when components are removed', function() {
+ const innerTemplate = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text('inner');
+ elementClose('div');
+ },
+ };
+ let innerUnmounted = 0;
+ const InnerComponent = createComponent(innerTemplate, undefined, {
+ componentWillUnmount() {
+ innerUnmounted++;
+ },
+ });
+ const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ component(InnerComponent, {});
+ elementClose('div');
+ },
+ };
+ let unmounted = 0;
+ const MyComponent = createComponent(template, undefined, {
+ componentWillUnmount() {
+ unmounted++;
+ },
+ });
+
+ const root = document.createElement('div');
+ render(root, MyComponent);
+ assert.equal(unmounted, 0);
+ assert.equal(innerUnmounted, 0);
+
+ unmountComponentAtNode(root);
+ assert.equal(unmounted, 1);
+ assert.equal(innerUnmounted, 1);
+ });
+
+ it('should remove node data', function() {
+ const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ text('yay');
+ elementClose('div');
+ },
+ };
+ const MyComponent = createComponent(template);
+
+ const root = document.createElement('div');
+ render(root, MyComponent);
+ assert(!!root['__incrementalDOMData']);
+
+ unmountComponentAtNode(root);
+ assert(!root['__incrementalDOMData']);
+ });
+ });
+});
diff --git a/packages/melody-component/__tests__/__snapshots__/EnsureKeySpec.js.snap b/packages/melody-component/__tests__/__snapshots__/EnsureKeySpec.js.snap
new file mode 100644
index 0000000..707316a
--- /dev/null
+++ b/packages/melody-component/__tests__/__snapshots__/EnsureKeySpec.js.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`children should rerender 1`] = `"Some element without key 0 and 1 1 and 1 2 and 1 3 and 1 4 and 1
"`;
+
+exports[`children should rerender after parent rerender 1`] = `"Some element without key 1 and 1 2 and 1 3 and 1 4 and 1 5 and 1
"`;
+
+exports[`parent should rerender 1`] = `"Some element without key 1 and 0 2 and 0 3 and 0 4 and 0 5 and 0
"`;
+
+exports[`parent should rerender after children rerender 1`] = `"Some element without key 1 and 1 2 and 1 3 and 1 4 and 1 5 and 1
"`;
+
+exports[`should mount on top of existing keyed components 1`] = `"Some element without key 0 and 0 1 and 0 2 and 0 3 and 0 4 and 0
"`;
diff --git a/packages/melody-component/package.json b/packages/melody-component/package.json
new file mode 100644
index 0000000..aa55250
--- /dev/null
+++ b/packages/melody-component/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "melody-component",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js",
+ "coverage": "nyc npm run test"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "melody-idom": "^0.10.0"
+ },
+ "devDependencies": {
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-component/src/actions.js b/packages/melody-component/src/actions.js
new file mode 100644
index 0000000..a144a4e
--- /dev/null
+++ b/packages/melody-component/src/actions.js
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import type { Component, ReceivePropsAction } from './index.js.flow';
+
+/**
+ * The `type` of the action which is triggered when the properties of
+ * a component are changed.
+ *
+ * Actions of this type follow the "Standard Flux Action" pattern. They have
+ * a property `type`, equal to this value, and a property `payload` which is
+ * an object containing the new properties.
+ *
+ * @type {string}
+ */
+export const RECEIVE_PROPS = 'MELODY/RECEIVE_PROPS';
+
+/**
+ * An Action Creator which creates an {@link RECEIVE_PROPS} action.
+ * @param payload The new properties
+ * @param meta The component which will receive new properties
+ * @returns ReceivePropsAction
+ */
+export function setProps(payload: Object, meta: Component): ReceivePropsAction {
+ return {
+ type: RECEIVE_PROPS,
+ payload,
+ meta,
+ };
+}
diff --git a/packages/melody-component/src/component.js b/packages/melody-component/src/component.js
new file mode 100644
index 0000000..1a0021d
--- /dev/null
+++ b/packages/melody-component/src/component.js
@@ -0,0 +1,195 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import type { Reducer, Action, Template } from './index.js.flow';
+
+import { mixin } from './mixin';
+import { createState } from './state';
+import { setProps, RECEIVE_PROPS } from './actions';
+import shallowEquals from './shallowEquals';
+import { enqueueComponent, options } from 'melody-idom';
+
+// type ComponentImpl = {
+// /**
+// * The element associated with this component.
+// */
+// el: Node,
+// /**
+// * A map of references to native HTML elements.
+// */
+// refs: { [key: string]: Element },
+
+// /**
+// * Set new properties for the Component.
+// * This might cause the component to request an update.
+// */
+// apply(props: any): void,
+// /**
+// * Executed after a component has been mounted or updated.
+// * After this method has been triggered, the component is considered stable and
+// * accessing its own DOM should be safe.
+// * The children of this Component might not have rendered.
+// */
+// notify(): void,
+// /**
+// * Invoked when a component should render itself.
+// */
+// render(): void
+// };
+
+function Component(element: Element, reducer: Reducer) {
+ // part of the public API
+ this.el = element;
+ this.refs = Object.create(null);
+ // needed for this type of component
+ this.props = null;
+ this.oldProps = null;
+ this.oldState = null;
+ this['MELODY/STORE'] = createState(reducer);
+ this.isMounted = false;
+ this.dispatch = this.dispatch.bind(this);
+ this.getState = this.getState.bind(this);
+ this.state = this.getState();
+ this.componentDidInitialize();
+ this.componentWillMount();
+}
+Object.assign(Component.prototype, {
+ /**
+ * Set new properties for the Component.
+ * This might cause the component to request an update.
+ */
+ apply(props) {
+ if (!this.oldProps) {
+ this.oldProps = this.props;
+ }
+ this.dispatch(setProps(props, this));
+ },
+
+ /**
+ * Executed after a component has been mounted or updated.
+ * After this method has been triggered, the component is considered stable and
+ * accessing the DOM should be safe.
+ * The children of this Component might not have been rendered.
+ */
+ notify() {
+ if (this.isMounted) {
+ this.componentDidUpdate(
+ this.oldProps || this.props,
+ this.oldState || this.state,
+ );
+ if (options.afterUpdate) {
+ options.afterUpdate(this);
+ }
+ } else {
+ this.isMounted = true;
+ this.componentDidMount();
+ if (options.afterMount) {
+ options.afterMount(this);
+ }
+ }
+ this.oldProps = null;
+ this.oldState = null;
+ },
+
+ dispatch(action: Action) {
+ const newState = this['MELODY/STORE'](action);
+ let newProps = this.props;
+ const isReceiveProps = action.type === RECEIVE_PROPS;
+ if (isReceiveProps) {
+ newProps = action.payload;
+ }
+ const shouldUpdate =
+ (isReceiveProps && !this.isMounted) ||
+ (this.el && this.shouldComponentUpdate(newProps, newState));
+ if (shouldUpdate && this.isMounted) {
+ this.componentWillUpdate(newProps, newState);
+ }
+ if (isReceiveProps) {
+ this.props = newProps;
+ }
+ if (shouldUpdate) {
+ enqueueComponent(this);
+ }
+ return newState || this.state;
+ },
+
+ getState() {
+ return this['MELODY/STORE']();
+ },
+
+ shouldComponentUpdate(nextProps: Object, nextState: Object) {
+ return !shallowEquals(this.state, nextState);
+ },
+
+ /**
+ * Invoked when a component should render itself.
+ */
+ render() {},
+ componentDidInitialize() {},
+ componentWillMount() {},
+ componentDidMount() {},
+ componentWillUpdate() {},
+ componentDidUpdate(prevProps: Object, prevState: Object) {},
+ /**
+ * Invoked before a component is unmounted.
+ */
+ componentWillUnmount() {},
+});
+
+function mapPropsToState(state, action) {
+ return action.type === RECEIVE_PROPS ? action.payload : state || {};
+}
+
+function createComponentConstructor(Parent, parentReducer) {
+ function ChildComponent(el, reducer: ?Reducer) {
+ if (!this || !(this instanceof ChildComponent)) {
+ const EnhancedChild = createComponentConstructor(
+ ChildComponent,
+ parentReducer,
+ );
+ for (let i = 0, len = arguments.length; i < len; i++) {
+ mixin(EnhancedChild, arguments[i]);
+ }
+ return EnhancedChild;
+ }
+ Parent.call(this, el, reducer || parentReducer);
+ }
+ ChildComponent.prototype = Object.create(Parent.prototype, {
+ constructor: { value: ChildComponent },
+ });
+ return ChildComponent;
+}
+
+export function createComponent(
+ templateFnOrObj: Template,
+ reducer: ?Reducer,
+): Component {
+ const template = templateFnOrObj.render
+ ? props => templateFnOrObj.render(props)
+ : templateFnOrObj;
+ const finalReducer = reducer || mapPropsToState;
+ const ChildComponent = createComponentConstructor(Component, finalReducer);
+ ChildComponent.prototype.displayName =
+ template.name || template.displayName || 'Unknown';
+ ChildComponent.prototype.render = function() {
+ this.oldState = this.state;
+ this.state = this.getState();
+ return template(this.state);
+ };
+ for (let i = 2, len = arguments.length; i < len; i++) {
+ mixin(ChildComponent, arguments[i]);
+ }
+ return ChildComponent;
+}
diff --git a/packages/melody-component/src/index.js b/packages/melody-component/src/index.js
new file mode 100644
index 0000000..5f7029f
--- /dev/null
+++ b/packages/melody-component/src/index.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { createComponent } from './component';
+export { setProps, RECEIVE_PROPS } from './actions';
+export { render, unmountComponentAtNode } from './render';
diff --git a/packages/melody-component/src/index.js.flow b/packages/melody-component/src/index.js.flow
new file mode 100644
index 0000000..ac36267
--- /dev/null
+++ b/packages/melody-component/src/index.js.flow
@@ -0,0 +1,30 @@
+/**
+ * An Action is a simple object which describes the requested state change.
+ * It must have a `type` property and should follow the "Standard Flux Action" pattern.
+ */
+export type Action = { type: String };
+/**
+ * A Reducer is a function which returns a new state based on a previous state
+ * and an action.
+ */
+export type Reducer = (state: ?Object, action: Action) => Object;
+/**
+ * Basic interface for idom templates.
+ */
+export type Template = {render: () => void};
+/**
+ * A mixin is a capability that can be added to a component.
+ */
+export type Mixin = void | Object | (proto: Object) => Object;
+
+export type ReduxStore = {
+ subscribe: () => void,
+ dispatch: (action: Action) => void,
+ getState: () => Object
+};
+
+export type ReceivePropsAction = {
+ type: 'MELODY/RECEIVE_PROPS',
+ payload: Object,
+ meta: Component
+};
diff --git a/packages/melody-component/src/mixin.js b/packages/melody-component/src/mixin.js
new file mode 100644
index 0000000..40e8b98
--- /dev/null
+++ b/packages/melody-component/src/mixin.js
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Utility function to add capabilities to an object. Such a capability
+ * is usually called a "mixin" and can be either
+ *
+ * - an object that is merged into the prototype of `target`
+ * - a function taking the prototype and optionally returning an object which
+ * is merged into the prototype
+ * - a falsy value (`false`, `null` or `undefined`) which is ignored.
+ * This is useful for adding a capability optionally.
+ *
+ * @param target The constructor of a class
+ * @param {...Mixin} mixins The mixins applied to the `target`
+ * @returns {*}
+ */
+export function mixin(target) {
+ var obj = typeof target === 'function' ? target.prototype : target;
+ // If implementation proves to be too slow, rewrite to use a proper loop
+ for (let i = 1, len = arguments.length; i < len; i++) {
+ const mixin = arguments[i];
+ mixin &&
+ Object.assign(
+ obj,
+ typeof mixin === 'function' ? mixin(obj) : mixin,
+ );
+ }
+ return target;
+}
diff --git a/packages/melody-component/src/render.js b/packages/melody-component/src/render.js
new file mode 100644
index 0000000..65083d3
--- /dev/null
+++ b/packages/melody-component/src/render.js
@@ -0,0 +1,51 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { mount, patchOuter, flush, unmountComponent } from 'melody-idom';
+
+export function render(el, Component, props) {
+ const result = patchOuter(el, () => {
+ mount(el, Component, props);
+ });
+ if (process.env.NODE_ENV === 'test') {
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ }
+ return result;
+}
+
+export function unmountComponentAtNode(node) {
+ if (!node) {
+ return;
+ }
+ const data = node['__incrementalDOMData'];
+ // No data? No component.
+ if (!data) {
+ return;
+ }
+ // No componentInstance? Unmounting not needed.
+ const { componentInstance } = data;
+ if (!componentInstance) {
+ return;
+ }
+ // Tear down components
+ unmountComponent(componentInstance);
+ // Remove node data
+ node['__incrementalDOMData'] = undefined;
+}
diff --git a/packages/melody-component/src/shallowEquals.js b/packages/melody-component/src/shallowEquals.js
new file mode 100644
index 0000000..4e83693
--- /dev/null
+++ b/packages/melody-component/src/shallowEquals.js
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const hasOwn = Object.prototype.hasOwnProperty;
+
+// based on react-redux
+export default function shallowEquals(a, b) {
+ if (a === b) {
+ return true;
+ }
+
+ if (!a || !b) {
+ return false;
+ }
+
+ const keyOfA = Object.keys(a),
+ keysOfB = Object.keys(b);
+
+ if (keyOfA.length !== keysOfB.length) {
+ return false;
+ }
+
+ for (let i = 0; i < keyOfA.length; i++) {
+ if (!hasOwn.call(b, keyOfA[i]) || a[keyOfA[i]] !== b[keyOfA[i]]) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/packages/melody-component/src/state.js b/packages/melody-component/src/state.js
new file mode 100644
index 0000000..a17a1a9
--- /dev/null
+++ b/packages/melody-component/src/state.js
@@ -0,0 +1,44 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import type { Reducer, Action } from './index.js.flow';
+
+export type State = (action: ?Action) => Object;
+
+/**
+ * A simple state container which is modified through actions
+ * by using a reducer.
+ *
+ * When the returned state function is invoked without parameters,
+ * it returns the current state.
+ *
+ * If the returned function is invoked with an action, the reducer is executed
+ * and its return value becomes the new state.
+ *
+ * @param reducer
+ * @returns {Function}
+ */
+export function createState(reducer: Reducer): State {
+ let state = reducer(undefined, {
+ type: 'MELODY/@@INIT',
+ });
+
+ return function store(action: ?Action): Object {
+ if (action) {
+ state = reducer(state, action) || state;
+ }
+ return state;
+ };
+}
diff --git a/packages/melody-devtools/package.json b/packages/melody-devtools/package.json
new file mode 100644
index 0000000..54b0042
--- /dev/null
+++ b/packages/melody-devtools/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "melody-devtools",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js",
+ "coverage": "nyc npm run test"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "melody-idom": "^0.10.0"
+ },
+ "devDependencies": {
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-devtools/src/index.js b/packages/melody-devtools/src/index.js
new file mode 100644
index 0000000..e800e82
--- /dev/null
+++ b/packages/melody-devtools/src/index.js
@@ -0,0 +1,442 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { getParent, getNodeData, enqueueComponent, options } from 'melody-idom';
+
+/**
+ * Return a ReactElement-compatible object for the current state of a preact
+ * component.
+ */
+function createReactElement(component) {
+ if (!component.constructor.displayName) {
+ component.constructor.displayName = component.displayName;
+ }
+ return {
+ type: component.constructor,
+ key: component.el && getNodeData(component.el).key,
+ ref: null, // Unsupported
+ props: component.props || component.ownProps,
+ };
+}
+
+/**
+ * Create a ReactDOMComponent-compatible object for a given DOM node rendered
+ * by preact.
+ *
+ * This implements the subset of the ReactDOMComponent interface that
+ * React DevTools requires in order to display DOM nodes in the inspector with
+ * the correct type and properties.
+ *
+ * @param {Node} node
+ */
+function createReactDOMComponent(node) {
+ const childNodes =
+ node.nodeType === Node.ELEMENT_NODE ? Array.from(node.childNodes) : [];
+
+ const isText = node.nodeType === Node.TEXT_NODE;
+
+ return {
+ // --- ReactDOMComponent interface
+ _currentElement: isText
+ ? node.textContent
+ : {
+ type: node.nodeName.toLowerCase(),
+ },
+ _renderedChildren: childNodes.map(child => {
+ const nodeData = getNodeData(child);
+ if (nodeData && nodeData.componentInstance) {
+ return updateReactComponent(nodeData.componentInstance);
+ }
+ return updateReactComponent(child);
+ }),
+ _stringText: isText ? node.textContent : null,
+ publicInstance: node,
+
+ // --- Additional properties used by preact devtools
+
+ // A flag indicating whether the devtools have been notified about the
+ // existence of this component instance yet.
+ // This is used to send the appropriate notifications when DOM components
+ // are added or updated between composite component updates.
+ _inDevTools: false,
+ node,
+ };
+}
+
+/**
+ * Return a ReactCompositeComponent-compatible object for a given preact
+ * component instance.
+ *
+ * This implements the subset of the ReactCompositeComponent interface that
+ * the DevTools requires in order to walk the component tree and inspect the
+ * component's properties.
+ *
+ * See https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/getData.js
+ */
+function createReactCompositeComponent(component) {
+ const _currentElement = createReactElement(component);
+ const node = component.el;
+
+ let instance = {
+ // --- ReactDOMComponent properties
+ name: component.displayName,
+ getName: () => component.displayName,
+ _currentElement,
+ props: component.props || component.ownProps,
+ state: component.state || component.renderProps,
+ // forceUpdate: () => {
+ // enqueueComponent(component);
+ // },
+ // setState: (state) => {
+ // if (component.state) {
+ // Object.assign(component.state, state);
+ // enqueueComponent(component);
+ // }
+ // },
+
+ // --- Additional properties used by preact devtools
+ node,
+ };
+
+ // React DevTools exposes the `_instance` field of the selected item in the
+ // component tree as `$r` in the console. `_instance` must refer to a
+ // React Component (or compatible) class instance with `props` and `state`
+ // fields and `setState()`, `forceUpdate()` methods.
+ //instance._instance = component;
+ instance._instance = {
+ get props() {
+ return component.props || component.ownProps;
+ },
+
+ get state() {
+ return component.state || component.renderProps;
+ },
+
+ get refs() {
+ return component.refs;
+ },
+
+ setState(state) {
+ Object.assign(component.state, state);
+ },
+
+ forceUpdate() {
+ enqueueComponent(component);
+ },
+
+ dispatch(action) {
+ if (component.dispatch) {
+ component.dispatch(action);
+ }
+ },
+ };
+
+ // If the root node returned by this component instance's render function
+ // was itself a composite component, there will be a `_component` property
+ // containing the child component instance.
+ if (component.childInstance) {
+ instance._renderedComponent = updateReactComponent(
+ component.childInstance,
+ );
+ } else if (node) {
+ // Otherwise, if the render() function returned an HTML/SVG element,
+ // create a ReactDOMComponent-like object for the DOM node itself.
+ instance._renderedComponent = updateReactComponent(node);
+ }
+
+ return instance;
+}
+
+/**
+ * Map of Component|Node to ReactDOMComponent|ReactCompositeComponent-like
+ * object.
+ *
+ * The same React*Component instance must be used when notifying devtools
+ * about the initial mount of a component and subsequent updates.
+ */
+const instanceMap = typeof Map === 'function' && new Map();
+
+/**
+ * Update (and create if necessary) the ReactDOMComponent|ReactCompositeComponent-like
+ * instance for a given preact component instance or DOM Node.
+ *
+ * @param {Component|Node} componentOrNode
+ */
+function updateReactComponent(componentOrNode) {
+ const newInstance =
+ componentOrNode instanceof Node
+ ? createReactDOMComponent(componentOrNode)
+ : createReactCompositeComponent(componentOrNode);
+ if (instanceMap.has(componentOrNode)) {
+ let inst = instanceMap.get(componentOrNode);
+ Object.assign(inst, newInstance);
+ return inst;
+ }
+ instanceMap.set(componentOrNode, newInstance);
+ return newInstance;
+}
+
+function nextRootKey(roots) {
+ return '.' + Object.keys(roots).length;
+}
+
+/**
+ * Find all root component instances rendered by preact in `node`'s children
+ * and add them to the `roots` map.
+ *
+ * @param {DOMElement} node
+ * @param {[key: string] => ReactDOMComponent|ReactCompositeComponent}
+ */
+function findRoots(node, roots) {
+ Array.from(node.childNodes).forEach(child => {
+ const nodeData = getNodeData(child);
+ if (nodeData && nodeData.componentInstance) {
+ roots[nextRootKey(roots)] = updateReactComponent(
+ nodeData.componentInstance,
+ );
+ } else {
+ findRoots(child, roots);
+ }
+ });
+}
+
+/**
+ * Create a bridge for exposing preact's component tree to React DevTools.
+ *
+ * It creates implementations of the interfaces that ReactDOM passes to
+ * devtools to enable it to query the component tree and hook into component
+ * updates.
+ *
+ * See https://github.com/facebook/react/blob/59ff7749eda0cd858d5ee568315bcba1be75a1ca/src/renderers/dom/ReactDOM.js
+ * for how ReactDOM exports its internals for use by the devtools and
+ * the `attachRenderer()` function in
+ * https://github.com/facebook/react-devtools/blob/e31ec5825342eda570acfc9bcb43a44258fceb28/backend/attachRenderer.js
+ * for how the devtools consumes the resulting objects.
+ */
+function createDevToolsBridge() {
+ // The devtools has different paths for interacting with the renderers from
+ // React Native, legacy React DOM and current React DOM.
+ //
+ // Here we emulate the interface for the current React DOM (v15+) lib.
+
+ // ReactDOMComponentTree-like object
+ const ComponentTree = {
+ getNodeFromInstance(instance) {
+ return instance.node;
+ },
+ getClosestInstanceFromNode(rootNode) {
+ let node = rootNode;
+ while (node && !getNodeData(node).componentInstance) {
+ node = node.parentNode;
+ }
+ return node
+ ? updateReactComponent(getNodeData(node).componentInstance)
+ : null;
+ },
+ };
+
+ // Map of root ID (the ID is unimportant) to component instance.
+ let roots = {};
+ findRoots(document.body, roots);
+
+ // ReactMount-like object
+ //
+ // Used by devtools to discover the list of root component instances and get
+ // notified when new root components are rendered.
+ const Mount = {
+ _instancesByReactRootID: roots,
+
+ // Stub - React DevTools expects to find this method and replace it
+ // with a wrapper in order to observe new root components being added
+ _renderNewRootComponent(/* instance, ... */) {},
+ };
+
+ // ReactReconciler-like object
+ const Reconciler = {
+ // Stubs - React DevTools expects to find these methods and replace them
+ // with wrappers in order to observe components being mounted, updated and
+ // unmounted
+ mountComponent(/* instance, ... */) {},
+ performUpdateIfNecessary(/* instance, ... */) {},
+ receiveComponent(/* instance, ... */) {},
+ unmountComponent(/* instance, ... */) {},
+ };
+
+ /** Notify devtools that a new component instance has been mounted into the DOM. */
+ const componentAdded = component => {
+ const instance = updateReactComponent(component);
+ if (isRootComponent(component)) {
+ instance._rootID = nextRootKey(roots);
+ roots[instance._rootID] = instance;
+ Mount._renderNewRootComponent(instance);
+ }
+ visitNonCompositeChildren(instance, childInst => {
+ childInst._inDevTools = true;
+ Reconciler.mountComponent(childInst);
+ });
+ Reconciler.mountComponent(instance);
+ };
+
+ /** Notify devtools that a component has been updated with new props/state. */
+ const componentUpdated = component => {
+ const prevRenderedChildren = [];
+ visitNonCompositeChildren(instanceMap.get(component), childInst => {
+ prevRenderedChildren.push(childInst);
+ });
+
+ // Notify devtools about updates to this component and any non-composite
+ // children
+ const instance = updateReactComponent(component);
+ Reconciler.receiveComponent(instance);
+ visitNonCompositeChildren(instance, childInst => {
+ if (!childInst._inDevTools) {
+ // New DOM child component
+ childInst._inDevTools = true;
+ Reconciler.mountComponent(childInst);
+ } else {
+ // Updated DOM child component
+ Reconciler.receiveComponent(childInst);
+ }
+ });
+
+ // For any non-composite children that were removed by the latest render,
+ // remove the corresponding ReactDOMComponent-like instances and notify
+ // the devtools
+ prevRenderedChildren.forEach(childInst => {
+ if (!document.body.contains(childInst.node)) {
+ instanceMap.delete(childInst.node);
+ Reconciler.unmountComponent(childInst);
+ }
+ });
+ };
+
+ /** Notify devtools that a component has been unmounted from the DOM. */
+ const componentRemoved = component => {
+ const instance = updateReactComponent(component);
+ visitNonCompositeChildren(childInst => {
+ instanceMap.delete(childInst.node);
+ Reconciler.unmountComponent(childInst);
+ });
+ Reconciler.unmountComponent(instance);
+ instanceMap.delete(component);
+ if (instance._rootID) {
+ delete roots[instance._rootID];
+ }
+ };
+
+ return {
+ componentAdded,
+ componentUpdated,
+ componentRemoved,
+
+ // Interfaces passed to devtools via __REACT_DEVTOOLS_GLOBAL_HOOK__.inject()
+ ComponentTree,
+ Mount,
+ Reconciler,
+ };
+}
+
+/**
+ * Return `true` if a preact component is a top level component rendered by
+ * `render()` into a container Element.
+ */
+function isRootComponent(component) {
+ const parentInstance = getParent(component);
+ if (parentInstance === undefined) {
+ return true;
+ } else if (parentInstance.childInstance !== undefined) {
+ return isRootComponent(parentInstance);
+ }
+ return false;
+}
+
+/**
+ * Visit all child instances of a ReactCompositeComponent-like object that are
+ * not composite components (ie. they represent DOM elements or text)
+ *
+ * @param {Component} component
+ * @param {(Component) => void} visitor
+ */
+function visitNonCompositeChildren(component, visitor) {
+ if (component._renderedComponent) {
+ if (!component._renderedComponent.componentInstance) {
+ visitor(component._renderedComponent);
+ visitNonCompositeChildren(component._renderedComponent, visitor);
+ }
+ } else if (component._renderedChildren) {
+ component._renderedChildren.forEach(child => {
+ visitor(child);
+ if (!child.componentInstance) {
+ visitNonCompositeChildren(child, visitor);
+ }
+ });
+ }
+}
+
+/**
+ * Create a bridge between the preact component tree and React's dev tools
+ * and register it.
+ *
+ * After this function is called, the React Dev Tools should be able to detect
+ * "React" on the page and show the component tree.
+ *
+ * This function hooks into preact VNode creation in order to expose functional
+ * components correctly, so it should be called before the root component(s)
+ * are rendered.
+ *
+ * Returns a cleanup function which unregisters the hooks.
+ */
+export function initDevTools() {
+ if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
+ // React DevTools are not installed
+ return;
+ }
+
+ // Notify devtools when preact components are mounted, updated or unmounted
+ const bridge = createDevToolsBridge();
+
+ const nextAfterMount = options.afterMount;
+ options.afterMount = component => {
+ bridge.componentAdded(component);
+ if (nextAfterMount) {
+ nextAfterMount(component);
+ }
+ };
+
+ const nextAfterUpdate = options.afterUpdate;
+ options.afterUpdate = component => {
+ bridge.componentUpdated(component);
+ if (nextAfterUpdate) {
+ nextAfterUpdate(component);
+ }
+ };
+
+ const nextBeforeUnmount = options.beforeUnmount;
+ options.beforeUnmount = component => {
+ bridge.componentRemoved(component);
+ if (nextBeforeUnmount) {
+ nextBeforeUnmount(component);
+ }
+ };
+
+ // Notify devtools about this instance of "React"
+ __REACT_DEVTOOLS_GLOBAL_HOOK__.inject(bridge);
+
+ return () => {
+ options.afterMount = nextAfterMount;
+ options.afterUpdate = nextAfterUpdate;
+ options.beforeUnmount = nextBeforeUnmount;
+ };
+}
diff --git a/packages/melody-extension-core/package.json b/packages/melody-extension-core/package.json
new file mode 100644
index 0000000..d36ed40
--- /dev/null
+++ b/packages/melody-extension-core/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "melody-extension-core",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; SUPPORT_CJS=true rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "babel-template": "^6.8.0",
+ "babel-types": "^6.8.1",
+ "lodash": "^4.12.0",
+ "shortid": "^2.2.6"
+ },
+ "bundledDependencies": [
+ "babel-types",
+ "babel-template",
+ "shortid"
+ ],
+ "peerDependencies": {
+ "melody-idom": "^0.10.0",
+ "melody-parser": "^0.10.0",
+ "melody-runtime": "^0.10.0",
+ "melody-traverse": "^0.10.0",
+ "melody-types": "^0.10.0"
+ },
+ "devDependencies": {
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-extension-core/src/index.js b/packages/melody-extension-core/src/index.js
new file mode 100644
index 0000000..51ce622
--- /dev/null
+++ b/packages/melody-extension-core/src/index.js
@@ -0,0 +1,166 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { unaryOperators, binaryOperators, tests } from './operators';
+import { AutoescapeParser } from './parser/autoescape';
+import { BlockParser } from './parser/block';
+import { DoParser } from './parser/do';
+import { EmbedParser } from './parser/embed';
+import { ExtendsParser } from './parser/extends';
+import { FilterParser } from './parser/filter';
+import { FlushParser } from './parser/flush';
+import { ForParser } from './parser/for';
+import { FromParser } from './parser/from';
+import { IfParser } from './parser/if';
+import { ImportParser } from './parser/import';
+import { IncludeParser } from './parser/include';
+import { MacroParser } from './parser/macro';
+import { SetParser } from './parser/set';
+import { SpacelessParser } from './parser/spaceless';
+import { UseParser } from './parser/use';
+import { MountParser } from './parser/mount';
+
+import forVisitor from './visitors/for';
+import testVisitor from './visitors/tests';
+import filters from './visitors/filters';
+import functions from './visitors/functions';
+
+const filterMap = [
+ 'attrs',
+ 'classes',
+ 'styles',
+ 'batch',
+ 'escape',
+ 'format',
+ 'merge',
+ 'nl2br',
+ 'number_format',
+ 'raw',
+ 'replace',
+ 'reverse',
+ 'round',
+ 'striptags',
+ 'title',
+ 'url_encode',
+].reduce((map, filterName) => {
+ map[filterName] = 'melody-runtime';
+ return map;
+}, Object.create(null));
+
+Object.assign(filterMap, filters);
+
+const functionMap = [
+ 'attribute',
+ 'constant',
+ 'cycle',
+ 'date',
+ 'max',
+ 'min',
+ 'random',
+ 'range',
+ 'source',
+ 'template_from_string',
+].reduce((map, functionName) => {
+ map[functionName] = 'melody-runtime';
+ return map;
+}, Object.create(null));
+Object.assign(functionMap, functions);
+
+export const extension = {
+ tags: [
+ AutoescapeParser,
+ BlockParser,
+ DoParser,
+ EmbedParser,
+ ExtendsParser,
+ FilterParser,
+ FlushParser,
+ ForParser,
+ FromParser,
+ IfParser,
+ ImportParser,
+ IncludeParser,
+ MacroParser,
+ SetParser,
+ SpacelessParser,
+ UseParser,
+ MountParser,
+ ],
+ unaryOperators,
+ binaryOperators,
+ tests,
+ visitors: [forVisitor, testVisitor],
+ filterMap,
+ functionMap,
+};
+
+export {
+ AutoescapeBlock,
+ BlockStatement,
+ BlockCallExpression,
+ MountStatement,
+ DoStatement,
+ EmbedStatement,
+ ExtendsStatement,
+ FilterBlockStatement,
+ FlushStatement,
+ ForStatement,
+ ImportDeclaration,
+ FromStatement,
+ IfStatement,
+ IncludeStatement,
+ MacroDeclarationStatement,
+ VariableDeclarationStatement,
+ SetStatement,
+ SpacelessBlock,
+ AliasExpression,
+ UseStatement,
+ UnaryNotExpression,
+ UnaryNeqExpression,
+ UnaryPosExpression,
+ BinaryOrExpression,
+ BinaryAndExpression,
+ BitwiseOrExpression,
+ BitwiseXorExpression,
+ BitwiseAndExpression,
+ BinaryEqualsExpression,
+ BinaryNotEqualsExpression,
+ BinaryLessThanExpression,
+ BinaryGreaterThanExpression,
+ BinaryLessThanOrEqualExpression,
+ BinaryGreaterThanOrEqualExpression,
+ BinaryNotInExpression,
+ BinaryInExpression,
+ BinaryMatchesExpression,
+ BinaryStartsWithExpression,
+ BinaryEndsWithExpression,
+ BinaryRangeExpression,
+ BinaryAddExpression,
+ BinaryMulExpression,
+ BinaryDivExpression,
+ BinaryFloorDivExpression,
+ BinaryModExpression,
+ BinaryPowerExpression,
+ BinaryNullCoalesceExpression,
+ TestEvenExpression,
+ TestOddExpression,
+ TestDefinedExpression,
+ TestSameAsExpression,
+ TestNullExpression,
+ TestDivisibleByExpression,
+ TestConstantExpression,
+ TestEmptyExpression,
+ TestIterableExpression,
+} from './types';
diff --git a/packages/melody-extension-core/src/operators.js b/packages/melody-extension-core/src/operators.js
new file mode 100644
index 0000000..b4a65ae
--- /dev/null
+++ b/packages/melody-extension-core/src/operators.js
@@ -0,0 +1,402 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ Node,
+ BinaryExpression,
+ BinaryConcatExpression,
+ UnaryExpression,
+ type,
+ alias,
+ visitor,
+} from 'melody-types';
+import {
+ Types,
+ setStartFromToken,
+ setEndFromToken,
+ copyStart,
+ copyEnd,
+ copyLoc,
+ LEFT,
+} from 'melody-parser';
+
+export const unaryOperators = [];
+export const binaryOperators = [];
+export const tests = [];
+
+//region Unary Expressions
+export const UnaryNotExpression = createUnaryOperator(
+ 'not',
+ 'UnaryNotExpression',
+ 50,
+);
+export const UnaryNeqExpression = createUnaryOperator(
+ '-',
+ 'UnaryNeqExpression',
+ 500,
+);
+export const UnaryPosExpression = createUnaryOperator(
+ '+',
+ 'UnaryPosExpression',
+ 500,
+);
+//endregion
+
+//region Binary Expressions
+export const BinaryOrExpression = createBinaryOperatorNode({
+ text: 'or',
+ type: 'BinaryOrExpression',
+ precedence: 10,
+ associativity: LEFT,
+});
+export const BinaryAndExpression = createBinaryOperatorNode({
+ text: 'and',
+ type: 'BinaryAndExpression',
+ precedence: 15,
+ associativity: LEFT,
+});
+
+export const BitwiseOrExpression = createBinaryOperatorNode({
+ text: 'b-or',
+ type: 'BitwiseOrExpression',
+ precedence: 16,
+ associativity: LEFT,
+});
+export const BitwiseXorExpression = createBinaryOperatorNode({
+ text: 'b-xor',
+ type: 'BitwiseXOrExpression',
+ precedence: 17,
+ associativity: LEFT,
+});
+export const BitwiseAndExpression = createBinaryOperatorNode({
+ text: 'b-and',
+ type: 'BitwiseAndExpression',
+ precedence: 18,
+ associativity: LEFT,
+});
+
+export const BinaryEqualsExpression = createBinaryOperatorNode({
+ text: '==',
+ type: 'BinaryEqualsExpression',
+ precedence: 20,
+ associativity: LEFT,
+});
+export const BinaryNotEqualsExpression = createBinaryOperatorNode({
+ text: '!=',
+ type: 'BinaryNotEqualsExpression',
+ precedence: 20,
+ associativity: LEFT,
+});
+export const BinaryLessThanExpression = createBinaryOperatorNode({
+ text: '<',
+ type: 'BinaryLessThanExpression',
+ precedence: 20,
+ associativity: LEFT,
+});
+export const BinaryGreaterThanExpression = createBinaryOperatorNode({
+ text: '>',
+ type: 'BinaryGreaterThanExpression',
+ precedence: 20,
+ associativity: LEFT,
+});
+export const BinaryLessThanOrEqualExpression = createBinaryOperatorNode({
+ text: '<=',
+ type: 'BinaryLessThanOrEqualExpression',
+ precedence: 20,
+ associativity: LEFT,
+});
+export const BinaryGreaterThanOrEqualExpression = createBinaryOperatorNode({
+ text: '>=',
+ type: 'BinaryGreaterThanOrEqualExpression',
+ precedence: 20,
+ associativity: LEFT,
+});
+
+export const BinaryNotInExpression = createBinaryOperatorNode({
+ text: 'not in',
+ type: 'BinaryNotInExpression',
+ precedence: 20,
+ associativity: LEFT,
+});
+export const BinaryInExpression = createBinaryOperatorNode({
+ text: 'in',
+ type: 'BinaryInExpression',
+ precedence: 20,
+ associativity: LEFT,
+});
+export const BinaryMatchesExpression = createBinaryOperatorNode({
+ text: 'matches',
+ type: 'BinaryMatchesExpression',
+ precedence: 20,
+ associativity: LEFT,
+});
+export const BinaryStartsWithExpression = createBinaryOperatorNode({
+ text: 'starts with',
+ type: 'BinaryStartsWithExpression',
+ precedence: 20,
+ associativity: LEFT,
+});
+export const BinaryEndsWithExpression = createBinaryOperatorNode({
+ text: 'ends with',
+ type: 'BinaryEndsWithExpression',
+ precedence: 20,
+ associativity: LEFT,
+});
+
+export const BinaryRangeExpression = createBinaryOperatorNode({
+ text: '..',
+ type: 'BinaryRangeExpression',
+ precedence: 25,
+ associativity: LEFT,
+});
+
+export const BinaryAddExpression = createBinaryOperatorNode({
+ text: '+',
+ type: 'BinaryAddExpression',
+ precedence: 30,
+ associativity: LEFT,
+});
+export const BinarySubExpression = createBinaryOperatorNode({
+ text: '-',
+ type: 'BinarySubExpression',
+ precedence: 30,
+ associativity: LEFT,
+});
+binaryOperators.push({
+ text: '~',
+ precedence: 40,
+ associativity: LEFT,
+ createNode(token, lhs, rhs) {
+ const op = new BinaryConcatExpression(lhs, rhs);
+ copyStart(op, lhs);
+ copyEnd(op, rhs);
+ return op;
+ },
+});
+export const BinaryMulExpression = createBinaryOperatorNode({
+ text: '*',
+ type: 'BinaryMulExpression',
+ precedence: 60,
+ associativity: LEFT,
+});
+export const BinaryDivExpression = createBinaryOperatorNode({
+ text: '/',
+ type: 'BinaryDivExpression',
+ precedence: 60,
+ associativity: LEFT,
+});
+export const BinaryFloorDivExpression = createBinaryOperatorNode({
+ text: '//',
+ type: 'BinaryFloorDivExpression',
+ precedence: 60,
+ associativity: LEFT,
+});
+export const BinaryModExpression = createBinaryOperatorNode({
+ text: '%',
+ type: 'BinaryModExpression',
+ precedence: 60,
+ associativity: LEFT,
+});
+
+binaryOperators.push({
+ text: 'is',
+ precedence: 100,
+ associativity: LEFT,
+ parse(parser, token, expr) {
+ const tokens = parser.tokens;
+
+ let not = false;
+ if (tokens.nextIf(Types.OPERATOR, 'not')) {
+ not = true;
+ }
+
+ const test = getTest(parser);
+ let args = null;
+ if (tokens.test(Types.LPAREN)) {
+ args = parser.matchArguments();
+ }
+ const testExpression = test.createNode(expr, args);
+ setStartFromToken(testExpression, token);
+ setEndFromToken(testExpression, tokens.la(-1));
+ if (not) {
+ return copyLoc(
+ new UnaryNotExpression(testExpression),
+ testExpression,
+ );
+ }
+ return testExpression;
+ },
+});
+
+function getTest(parser) {
+ const tokens = parser.tokens;
+ const nameToken = tokens.la(0);
+ if (nameToken.type !== Types.NULL) {
+ tokens.expect(Types.SYMBOL);
+ } else {
+ tokens.next();
+ }
+ let testName = nameToken.text;
+ if (!parser.hasTest(testName)) {
+ // try 2-words tests
+ const continuedNameToken = tokens.expect(Types.SYMBOL);
+ testName += ' ' + continuedNameToken.text;
+ if (!parser.hasTest(testName)) {
+ parser.error({
+ title: `Unknown test "${testName}"`,
+ pos: nameToken.pos,
+ });
+ }
+ }
+
+ return parser.getTest(testName);
+}
+
+export const BinaryPowerExpression = createBinaryOperatorNode({
+ text: '**',
+ type: 'BinaryPowerExpression',
+ precedence: 200,
+ associativity: LEFT,
+});
+export const BinaryNullCoalesceExpression = createBinaryOperatorNode({
+ text: '??',
+ type: 'BinaryNullCoalesceExpression',
+ precedence: 300,
+ associativity: LEFT,
+});
+//endregion
+
+//region Test Expressions
+export const TestEvenExpression = createTest('even', 'TestEvenExpression');
+export const TestOddExpression = createTest('odd', 'TestOddExpression');
+export const TestDefinedExpression = createTest(
+ 'defined',
+ 'TestDefinedExpression',
+);
+export const TestSameAsExpression = createTest(
+ 'same as',
+ 'TestSameAsExpression',
+);
+tests.push({
+ text: 'sameas',
+ createNode(expr, args) {
+ // todo: add deprecation warning
+ return new TestSameAsExpression(expr, args);
+ },
+});
+export const TestNullExpression = createTest('null', 'TestNullExpression');
+tests.push({
+ text: 'none',
+ createNode(expr, args) {
+ return new TestNullExpression(expr, args);
+ },
+});
+export const TestDivisibleByExpression = createTest(
+ 'divisible by',
+ 'TestDivisibleByExpression',
+);
+tests.push({
+ text: 'divisibleby',
+ createNode(expr, args) {
+ // todo: add deprecation warning
+ return new TestDivisibleByExpression(expr, args);
+ },
+});
+export const TestConstantExpression = createTest(
+ 'constant',
+ 'TestConstantExpression',
+);
+export const TestEmptyExpression = createTest('empty', 'TestEmptyExpression');
+export const TestIterableExpression = createTest(
+ 'iterable',
+ 'TestIterableExpression',
+);
+//endregion
+
+//region Utilities
+function createTest(text, typeName) {
+ const TestExpression = class extends Node {
+ constructor(expr: Node, args?: Array) {
+ super();
+ this.expression = expr;
+ this.arguments = args;
+ }
+ };
+ type(TestExpression, typeName);
+ alias(TestExpression, 'Expression', 'TestExpression');
+ visitor(TestExpression, 'expression', 'arguments');
+
+ tests.push({
+ text,
+ createNode(expr, args) {
+ return new TestExpression(expr, args);
+ },
+ });
+
+ return TestExpression;
+}
+
+function createBinaryOperatorNode(options) {
+ const { text, precedence, associativity } = options;
+ const BinarySubclass = class extends BinaryExpression {
+ constructor(left: Node, right: Node) {
+ super(text, left, right);
+ }
+ };
+ type(BinarySubclass, options.type);
+ alias(BinarySubclass, 'BinaryExpression', 'Binary', 'Expression');
+ visitor(BinarySubclass, 'left', 'right');
+
+ const operator = {
+ text,
+ precedence,
+ associativity,
+ };
+ if (options.parse) {
+ operator.parse = options.parse;
+ } else if (options.createNode) {
+ operator.createNode = options.createNode;
+ } else {
+ operator.createNode = (token, lhs, rhs) => new BinarySubclass(lhs, rhs);
+ }
+ binaryOperators.push(operator);
+
+ return BinarySubclass;
+}
+
+function createUnaryOperator(operator, typeName, precedence) {
+ const UnarySubclass = class extends UnaryExpression {
+ constructor(argument: Node) {
+ super(operator, argument);
+ }
+ };
+ type(UnarySubclass, typeName);
+ alias(UnarySubclass, 'Expression', 'UnaryLike');
+ visitor(UnarySubclass, 'argument');
+
+ unaryOperators.push({
+ text: operator,
+ precedence,
+ createNode(token, expr) {
+ const op = new UnarySubclass(expr);
+ setStartFromToken(op, token);
+ copyEnd(op, expr);
+ return op;
+ },
+ });
+
+ return UnarySubclass;
+}
+//endregion
diff --git a/packages/melody-extension-core/src/parser/autoescape.js b/packages/melody-extension-core/src/parser/autoescape.js
new file mode 100644
index 0000000..99d4122
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/autoescape.js
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Types, setStartFromToken, setEndFromToken } from 'melody-parser';
+import { AutoescapeBlock } from './../types';
+
+export const AutoescapeParser = {
+ name: 'autoescape',
+ parse(parser, token) {
+ const tokens = parser.tokens;
+
+ let escapeType = null,
+ stringStartToken;
+ if (tokens.nextIf(Types.TAG_END)) {
+ escapeType = null;
+ } else if ((stringStartToken = tokens.nextIf(Types.STRING_START))) {
+ escapeType = tokens.expect(Types.STRING).text;
+ if (!tokens.nextIf(Types.STRING_END)) {
+ parser.error({
+ title:
+ 'autoescape type declaration must be a simple string',
+ pos: tokens.la(0).pos,
+ advice: `The type declaration for autoescape must be a simple string such as 'html' or 'js'.
+I expected the current string to end with a ${stringStartToken.text} but instead found ${Types
+ .ERROR_TABLE[tokens.lat(0)] || tokens.lat(0)}.`,
+ });
+ }
+ } else if (tokens.nextIf(Types.FALSE)) {
+ escapeType = false;
+ } else if (tokens.nextIf(Types.TRUE)) {
+ escapeType = true;
+ } else {
+ parser.error({
+ title: 'Invalid autoescape type declaration',
+ pos: tokens.la(0).pos,
+ advice: `Expected type of autoescape to be a string, boolean or not specified. Found ${tokens.la(
+ 0,
+ ).type} instead.`,
+ });
+ }
+
+ const autoescape = new AutoescapeBlock(escapeType);
+ setStartFromToken(autoescape, token);
+ let tagEndToken;
+ autoescape.expressions = parser.parse((_, token, tokens) => {
+ if (
+ token.type === Types.TAG_START &&
+ tokens.nextIf(Types.SYMBOL, 'endautoescape')
+ ) {
+ tagEndToken = tokens.expect(Types.TAG_END);
+ return true;
+ }
+ return false;
+ }).expressions;
+ setEndFromToken(autoescape, tagEndToken);
+
+ return autoescape;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/block.js b/packages/melody-extension-core/src/parser/block.js
new file mode 100644
index 0000000..1f626c5
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/block.js
@@ -0,0 +1,72 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Identifier, PrintExpressionStatement } from 'melody-types';
+import {
+ Types,
+ setStartFromToken,
+ setEndFromToken,
+ createNode,
+} from 'melody-parser';
+import { BlockStatement } from './../types';
+
+export const BlockParser = {
+ name: 'block',
+ parse(parser, token) {
+ const tokens = parser.tokens,
+ nameToken = tokens.expect(Types.SYMBOL);
+
+ let blockStatement;
+ if (tokens.nextIf(Types.TAG_END)) {
+ blockStatement = new BlockStatement(
+ createNode(Identifier, nameToken, nameToken.text),
+ parser.parse((tokenText, token, tokens) => {
+ return !!(
+ token.type === Types.TAG_START &&
+ tokens.nextIf(Types.SYMBOL, 'endblock')
+ );
+ }).expressions,
+ );
+
+ if (tokens.nextIf(Types.SYMBOL, nameToken.text)) {
+ if (tokens.lat(0) !== Types.TAG_END) {
+ const unexpectedToken = tokens.next();
+ parser.error({
+ title: 'Block name mismatch',
+ pos: unexpectedToken.pos,
+ advice:
+ unexpectedToken.type == Types.SYMBOL
+ ? `Expected end of block ${nameToken.text} but instead found end of block ${tokens.la(
+ 0,
+ ).text}.`
+ : `endblock must be followed by either '%}' or the name of the open block. Found a token of type ${Types
+ .ERROR_TABLE[unexpectedToken.type] ||
+ unexpectedToken.type} instead.`,
+ });
+ }
+ }
+ } else {
+ blockStatement = new BlockStatement(
+ createNode(Identifier, nameToken, nameToken.text),
+ new PrintExpressionStatement(parser.matchExpression()),
+ );
+ }
+
+ setStartFromToken(blockStatement, token);
+ setEndFromToken(blockStatement, tokens.expect(Types.TAG_END));
+
+ return blockStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/do.js b/packages/melody-extension-core/src/parser/do.js
new file mode 100644
index 0000000..9853a1e
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/do.js
@@ -0,0 +1,28 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Types, setStartFromToken, setEndFromToken } from 'melody-parser';
+import { DoStatement } from './../types';
+
+export const DoParser = {
+ name: 'do',
+ parse(parser, token) {
+ const tokens = parser.tokens,
+ doStatement = new DoStatement(parser.matchExpression());
+ setStartFromToken(doStatement, token);
+ setEndFromToken(doStatement, tokens.expect(Types.TAG_END));
+ return doStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/embed.js b/packages/melody-extension-core/src/parser/embed.js
new file mode 100644
index 0000000..d7e8928
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/embed.js
@@ -0,0 +1,58 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Node } from 'melody-types';
+import { Types, setStartFromToken, setEndFromToken } from 'melody-parser';
+import { filter } from 'lodash';
+import { EmbedStatement } from './../types';
+
+export const EmbedParser = {
+ name: 'embed',
+ parse(parser, token) {
+ const tokens = parser.tokens;
+
+ const embedStatement = new EmbedStatement(parser.matchExpression());
+
+ if (tokens.nextIf(Types.SYMBOL, 'ignore')) {
+ tokens.expect(Types.SYMBOL, 'missing');
+ embedStatement.ignoreMissing = true;
+ }
+
+ if (tokens.nextIf(Types.SYMBOL, 'with')) {
+ embedStatement.argument = parser.matchExpression();
+ }
+
+ if (tokens.nextIf(Types.SYMBOL, 'only')) {
+ embedStatement.contextFree = true;
+ }
+
+ tokens.expect(Types.TAG_END);
+
+ embedStatement.blocks = filter(
+ parser.parse((tokenText, token, tokens) => {
+ return !!(
+ token.type === Types.TAG_START &&
+ tokens.nextIf(Types.SYMBOL, 'endembed')
+ );
+ }).expressions,
+ Node.isBlockStatement,
+ );
+
+ setStartFromToken(embedStatement, token);
+ setEndFromToken(embedStatement, tokens.expect(Types.TAG_END));
+
+ return embedStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/extends.js b/packages/melody-extension-core/src/parser/extends.js
new file mode 100644
index 0000000..0a0f737
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/extends.js
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Types, setStartFromToken, setEndFromToken } from 'melody-parser';
+import { ExtendsStatement } from './../types';
+
+export const ExtendsParser = {
+ name: 'extends',
+ parse(parser, token) {
+ const tokens = parser.tokens;
+
+ const extendsStatement = new ExtendsStatement(parser.matchExpression());
+
+ setStartFromToken(extendsStatement, token);
+ setEndFromToken(extendsStatement, tokens.expect(Types.TAG_END));
+
+ return extendsStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/filter.js b/packages/melody-extension-core/src/parser/filter.js
new file mode 100644
index 0000000..0c08836
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/filter.js
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Identifier } from 'melody-types';
+import {
+ Types,
+ setStartFromToken,
+ setEndFromToken,
+ createNode,
+} from 'melody-parser';
+import { FilterBlockStatement } from './../types';
+
+export const FilterParser = {
+ name: 'filter',
+ parse(parser, token) {
+ const tokens = parser.tokens,
+ ref = createNode(Identifier, token, 'filter'),
+ filterExpression = parser.matchFilterExpression(ref);
+ tokens.expect(Types.TAG_END);
+ const body = parser.parse((text, token, tokens) => {
+ return (
+ token.type === Types.TAG_START &&
+ tokens.nextIf(Types.SYMBOL, 'endfilter')
+ );
+ }).expressions;
+
+ const filterBlockStatement = new FilterBlockStatement(
+ filterExpression,
+ body,
+ );
+ setStartFromToken(filterBlockStatement, token);
+ setEndFromToken(filterBlockStatement, tokens.expect(Types.TAG_END));
+ return filterBlockStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/flush.js b/packages/melody-extension-core/src/parser/flush.js
new file mode 100644
index 0000000..6381a4d
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/flush.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Types, setStartFromToken, setEndFromToken } from 'melody-parser';
+import { FlushStatement } from './../types';
+
+export const FlushParser = {
+ name: 'flush',
+ parse(parser, token) {
+ const tokens = parser.tokens,
+ flushStatement = new FlushStatement();
+
+ setStartFromToken(flushStatement, token);
+ setEndFromToken(flushStatement, tokens.expect(Types.TAG_END));
+ return flushStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/for.js b/packages/melody-extension-core/src/parser/for.js
new file mode 100644
index 0000000..f934f89
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/for.js
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Identifier } from 'melody-types';
+import {
+ Types,
+ setStartFromToken,
+ setEndFromToken,
+ createNode,
+} from 'melody-parser';
+import { ForStatement } from './../types';
+
+export const ForParser = {
+ name: 'for',
+ parse(parser, token) {
+ const tokens = parser.tokens,
+ forStatement = new ForStatement();
+
+ const keyTarget = tokens.expect(Types.SYMBOL);
+ if (tokens.nextIf(Types.COMMA)) {
+ forStatement.keyTarget = createNode(
+ Identifier,
+ keyTarget,
+ keyTarget.text,
+ );
+ const valueTarget = tokens.expect(Types.SYMBOL);
+ forStatement.valueTarget = createNode(
+ Identifier,
+ valueTarget,
+ valueTarget.text,
+ );
+ } else {
+ forStatement.keyTarget = null;
+ forStatement.valueTarget = createNode(
+ Identifier,
+ keyTarget,
+ keyTarget.text,
+ );
+ }
+
+ tokens.expect(Types.OPERATOR, 'in');
+
+ forStatement.sequence = parser.matchExpression();
+
+ if (tokens.nextIf(Types.SYMBOL, 'if')) {
+ forStatement.condition = parser.matchExpression();
+ }
+
+ tokens.expect(Types.TAG_END);
+
+ forStatement.body = parser.parse((tokenText, token, tokens) => {
+ return (
+ token.type === Types.TAG_START &&
+ (tokens.test(Types.SYMBOL, 'else') ||
+ tokens.test(Types.SYMBOL, 'endfor'))
+ );
+ });
+
+ if (tokens.nextIf(Types.SYMBOL, 'else')) {
+ tokens.expect(Types.TAG_END);
+ forStatement.otherwise = parser.parse(
+ (tokenText, token, tokens) => {
+ return (
+ token.type === Types.TAG_START &&
+ tokens.test(Types.SYMBOL, 'endfor')
+ );
+ },
+ );
+ }
+ tokens.expect(Types.SYMBOL, 'endfor');
+
+ setStartFromToken(forStatement, token);
+ setEndFromToken(forStatement, tokens.expect(Types.TAG_END));
+
+ return forStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/from.js b/packages/melody-extension-core/src/parser/from.js
new file mode 100644
index 0000000..cd51781
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/from.js
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Identifier } from 'melody-types';
+import {
+ Types,
+ setStartFromToken,
+ setEndFromToken,
+ createNode,
+} from 'melody-parser';
+import { ImportDeclaration, FromStatement } from './../types';
+
+export const FromParser = {
+ name: 'from',
+ parse(parser, token) {
+ const tokens = parser.tokens,
+ source = parser.matchExpression(),
+ imports = [];
+
+ tokens.expect(Types.SYMBOL, 'import');
+
+ do {
+ const name = tokens.expect(Types.SYMBOL);
+
+ let alias = name;
+ if (tokens.nextIf(Types.SYMBOL, 'as')) {
+ alias = tokens.expect(Types.SYMBOL);
+ }
+
+ const importDeclaration = new ImportDeclaration(
+ createNode(Identifier, name, name.text),
+ createNode(Identifier, alias, alias.text),
+ );
+ setStartFromToken(importDeclaration, name);
+ setEndFromToken(importDeclaration, alias);
+
+ imports.push(importDeclaration);
+
+ if (!tokens.nextIf(Types.COMMA)) {
+ break;
+ }
+ } while (!tokens.test(Types.EOF));
+
+ const fromStatement = new FromStatement(source, imports);
+
+ setStartFromToken(fromStatement, token);
+ setEndFromToken(fromStatement, tokens.expect(Types.TAG_END));
+
+ return fromStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/if.js b/packages/melody-extension-core/src/parser/if.js
new file mode 100644
index 0000000..03ac44d
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/if.js
@@ -0,0 +1,69 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Types, setStartFromToken, setEndFromToken } from 'melody-parser';
+import { IfStatement } from './../types';
+
+export const IfParser = {
+ name: 'if',
+ parse(parser, token) {
+ const tokens = parser.tokens;
+ let test = parser.matchExpression(),
+ alternate = null;
+
+ tokens.expect(Types.TAG_END);
+
+ const ifStatement = new IfStatement(
+ test,
+ parser.parse(matchConsequent).expressions,
+ );
+
+ do {
+ if (tokens.nextIf(Types.SYMBOL, 'else')) {
+ tokens.expect(Types.TAG_END);
+ (alternate || ifStatement).alternate = parser.parse(
+ matchAlternate,
+ ).expressions;
+ } else if (tokens.nextIf(Types.SYMBOL, 'elseif')) {
+ test = parser.matchExpression();
+ tokens.expect(Types.TAG_END);
+ const consequent = parser.parse(matchConsequent).expressions;
+ alternate = (alternate || ifStatement
+ ).alternate = new IfStatement(test, consequent);
+ }
+
+ if (tokens.nextIf(Types.SYMBOL, 'endif')) {
+ break;
+ }
+ } while (!tokens.test(Types.EOF));
+
+ setStartFromToken(ifStatement, token);
+ setEndFromToken(ifStatement, tokens.expect(Types.TAG_END));
+
+ return ifStatement;
+ },
+};
+
+function matchConsequent(tokenText, token, tokens) {
+ if (token.type === Types.TAG_START) {
+ const next = tokens.la(0).text;
+ return next === 'else' || next === 'endif' || next === 'elseif';
+ }
+ return false;
+}
+
+function matchAlternate(tokenText, token, tokens) {
+ return token.type === Types.TAG_START && tokens.test(Types.SYMBOL, 'endif');
+}
diff --git a/packages/melody-extension-core/src/parser/import.js b/packages/melody-extension-core/src/parser/import.js
new file mode 100644
index 0000000..73c5721
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/import.js
@@ -0,0 +1,44 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Identifier } from 'melody-types';
+import {
+ Types,
+ setStartFromToken,
+ setEndFromToken,
+ createNode,
+} from 'melody-parser';
+import { ImportDeclaration } from './../types';
+
+export const ImportParser = {
+ name: 'import',
+ parse(parser, token) {
+ const tokens = parser.tokens,
+ source = parser.matchExpression();
+
+ tokens.expect(Types.SYMBOL, 'as');
+ const alias = tokens.expect(Types.SYMBOL);
+
+ const importStatement = new ImportDeclaration(
+ source,
+ createNode(Identifier, alias, alias.text),
+ );
+
+ setStartFromToken(importStatement, token);
+ setEndFromToken(importStatement, tokens.expect(Types.TAG_END));
+
+ return importStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/include.js b/packages/melody-extension-core/src/parser/include.js
new file mode 100644
index 0000000..6f465f8
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/include.js
@@ -0,0 +1,44 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Types, setStartFromToken, setEndFromToken } from 'melody-parser';
+import { IncludeStatement } from './../types';
+
+export const IncludeParser = {
+ name: 'include',
+ parse(parser, token) {
+ const tokens = parser.tokens;
+
+ const includeStatement = new IncludeStatement(parser.matchExpression());
+
+ if (tokens.nextIf(Types.SYMBOL, 'ignore')) {
+ tokens.expect(Types.SYMBOL, 'missing');
+ includeStatement.ignoreMissing = true;
+ }
+
+ if (tokens.nextIf(Types.SYMBOL, 'with')) {
+ includeStatement.argument = parser.matchExpression();
+ }
+
+ if (tokens.nextIf(Types.SYMBOL, 'only')) {
+ includeStatement.contextFree = true;
+ }
+
+ setStartFromToken(includeStatement, token);
+ setEndFromToken(includeStatement, tokens.expect(Types.TAG_END));
+
+ return includeStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/macro.js b/packages/melody-extension-core/src/parser/macro.js
new file mode 100644
index 0000000..f841576
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/macro.js
@@ -0,0 +1,81 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Identifier } from 'melody-types';
+import {
+ Types,
+ setStartFromToken,
+ setEndFromToken,
+ createNode,
+} from 'melody-parser';
+import { MacroDeclarationStatement } from './../types';
+
+export const MacroParser = {
+ name: 'macro',
+ parse(parser, token) {
+ const tokens = parser.tokens;
+
+ const nameToken = tokens.expect(Types.SYMBOL);
+ const args = [];
+
+ tokens.expect(Types.LPAREN);
+ while (!tokens.test(Types.RPAREN) && !tokens.test(Types.EOF)) {
+ const arg = tokens.expect(Types.SYMBOL);
+ args.push(createNode(Identifier, arg, arg.text));
+
+ if (!tokens.nextIf(Types.COMMA) && !tokens.test(Types.RPAREN)) {
+ // not followed by comma or rparen
+ parser.error({
+ title: 'Expected comma or ")"',
+ pos: tokens.la(0).pos,
+ advice:
+ 'The argument list of a macro can only consist of parameter names separated by commas.',
+ });
+ }
+ }
+ tokens.expect(Types.RPAREN);
+
+ const body = parser.parse((tokenText, token, tokens) => {
+ return !!(
+ token.type === Types.TAG_START &&
+ tokens.nextIf(Types.SYMBOL, 'endmacro')
+ );
+ });
+
+ if (tokens.test(Types.SYMBOL)) {
+ var nameEndToken = tokens.next();
+ if (nameToken.text !== nameEndToken.text) {
+ parser.error({
+ title: `Macro name mismatch, expected "${nameToken.text}" but found "${nameEndToken.text}"`,
+ pos: nameEndToken.pos,
+ });
+ }
+ }
+
+ const macroDeclarationStatement = new MacroDeclarationStatement(
+ createNode(Identifier, nameToken, nameToken.text),
+ args,
+ body,
+ );
+
+ setStartFromToken(macroDeclarationStatement, token);
+ setEndFromToken(
+ macroDeclarationStatement,
+ tokens.expect(Types.TAG_END),
+ );
+
+ return macroDeclarationStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/mount.js b/packages/melody-extension-core/src/parser/mount.js
new file mode 100644
index 0000000..a6d4b3d
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/mount.js
@@ -0,0 +1,60 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Identifier } from 'melody-types';
+import { MountStatement } from '../types';
+import {
+ Types,
+ setStartFromToken,
+ setEndFromToken,
+ createNode,
+} from 'melody-parser';
+
+export const MountParser = {
+ name: 'mount',
+ parse(parser, token) {
+ const tokens = parser.tokens;
+
+ let name = null,
+ source = null,
+ key = null,
+ argument = null;
+
+ if (tokens.test(Types.STRING_START)) {
+ source = parser.matchStringExpression();
+ } else {
+ const nameToken = tokens.expect(Types.SYMBOL);
+ name = createNode(Identifier, nameToken, nameToken.text);
+ if (tokens.nextIf(Types.SYMBOL, 'from')) {
+ source = parser.matchStringExpression();
+ }
+ }
+
+ if (tokens.nextIf(Types.SYMBOL, 'as')) {
+ key = parser.matchExpression();
+ }
+
+ if (tokens.nextIf(Types.SYMBOL, 'with')) {
+ argument = parser.matchExpression();
+ }
+
+ const mountStatement = new MountStatement(name, source, key, argument);
+
+ setStartFromToken(mountStatement, token);
+ setEndFromToken(mountStatement, tokens.expect(Types.TAG_END));
+
+ return mountStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/set.js b/packages/melody-extension-core/src/parser/set.js
new file mode 100644
index 0000000..6655070
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/set.js
@@ -0,0 +1,86 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Identifier } from 'melody-types';
+import {
+ Types,
+ setStartFromToken,
+ setEndFromToken,
+ createNode,
+} from 'melody-parser';
+import { VariableDeclarationStatement, SetStatement } from './../types';
+
+export const SetParser = {
+ name: 'set',
+ parse(parser, token) {
+ const tokens = parser.tokens,
+ names = [],
+ values = [];
+
+ do {
+ const name = tokens.expect(Types.SYMBOL);
+ names.push(createNode(Identifier, name, name.text));
+ } while (tokens.nextIf(Types.COMMA));
+
+ if (tokens.nextIf(Types.ASSIGNMENT)) {
+ do {
+ values.push(parser.matchExpression());
+ } while (tokens.nextIf(Types.COMMA));
+ } else {
+ if (names.length !== 1) {
+ parser.error({
+ title: 'Illegal multi-set',
+ pos: tokens.la(0).pos,
+ advice:
+ 'When using set with a block, you cannot have multiple targets.',
+ });
+ }
+ tokens.expect(Types.TAG_END);
+
+ values[0] = parser.parse((tokenText, token, tokens) => {
+ return !!(
+ token.type === Types.TAG_START &&
+ tokens.nextIf(Types.SYMBOL, 'endset')
+ );
+ }).expressions;
+ }
+
+ if (names.length !== values.length) {
+ parser.error({
+ title: 'Mismatch of set names and values',
+ pos: token.pos,
+ advice: `When using set, you must ensure that the number of
+assigned variable names is identical to the supplied values. However, here I've found
+${names.length} variable names and ${values.length} values.`,
+ });
+ }
+
+ // now join names and values
+ const assignments = [];
+ for (let i = 0, len = names.length; i < len; i++) {
+ assignments[i] = new VariableDeclarationStatement(
+ names[i],
+ values[i],
+ );
+ }
+
+ const setStatement = new SetStatement(assignments);
+
+ setStartFromToken(setStatement, token);
+ setEndFromToken(setStatement, tokens.expect(Types.TAG_END));
+
+ return setStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/spaceless.js b/packages/melody-extension-core/src/parser/spaceless.js
new file mode 100644
index 0000000..1cb154b
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/spaceless.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Types, setStartFromToken, setEndFromToken } from 'melody-parser';
+import { SpacelessBlock } from './../types';
+
+export const SpacelessParser = {
+ name: 'spaceless',
+ parse(parser, token) {
+ const tokens = parser.tokens;
+
+ tokens.expect(Types.TAG_END);
+
+ const body = parser.parse((tokenText, token, tokens) => {
+ return !!(
+ token.type === Types.TAG_START &&
+ tokens.nextIf(Types.SYMBOL, 'endspaceless')
+ );
+ }).expressions;
+
+ const spacelessBlock = new SpacelessBlock(body);
+ setStartFromToken(spacelessBlock, token);
+ setEndFromToken(spacelessBlock, tokens.expect(Types.TAG_END));
+
+ return spacelessBlock;
+ },
+};
diff --git a/packages/melody-extension-core/src/parser/use.js b/packages/melody-extension-core/src/parser/use.js
new file mode 100644
index 0000000..007a990
--- /dev/null
+++ b/packages/melody-extension-core/src/parser/use.js
@@ -0,0 +1,58 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Identifier } from 'melody-types';
+import {
+ Types,
+ setStartFromToken,
+ setEndFromToken,
+ copyStart,
+ copyEnd,
+ createNode,
+} from 'melody-parser';
+import { AliasExpression, UseStatement } from './../types';
+
+export const UseParser = {
+ name: 'use',
+ parse(parser, token) {
+ const tokens = parser.tokens;
+
+ const source = parser.matchExpression(),
+ aliases = [];
+
+ if (tokens.nextIf(Types.SYMBOL, 'with')) {
+ do {
+ const nameToken = tokens.expect(Types.SYMBOL),
+ name = createNode(Identifier, nameToken, nameToken.text);
+ let alias = name;
+ if (tokens.nextIf(Types.SYMBOL, 'as')) {
+ const aliasToken = tokens.expect(Types.SYMBOL);
+ alias = createNode(Identifier, aliasToken, aliasToken.text);
+ }
+ const aliasExpression = new AliasExpression(name, alias);
+ copyStart(aliasExpression, name);
+ copyEnd(aliasExpression, alias);
+ aliases.push(aliasExpression);
+ } while (tokens.nextIf(Types.COMMA));
+ }
+
+ const useStatement = new UseStatement(source, aliases);
+
+ setStartFromToken(useStatement, token);
+ setEndFromToken(useStatement, tokens.expect(Types.TAG_END));
+
+ return useStatement;
+ },
+};
diff --git a/packages/melody-extension-core/src/types.js b/packages/melody-extension-core/src/types.js
new file mode 100644
index 0000000..6b25894
--- /dev/null
+++ b/packages/melody-extension-core/src/types.js
@@ -0,0 +1,311 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ Node,
+ Identifier,
+ SequenceExpression,
+ type,
+ alias,
+ visitor,
+} from 'melody-types';
+import type { StringLiteral } from 'melody-types';
+
+export class AutoescapeBlock extends Node {
+ constructor(type: String | boolean, expressions?: Array) {
+ super();
+ this.escapeType = type;
+ this.expressions = expressions;
+ }
+}
+type(AutoescapeBlock, 'AutoescapeBlock');
+alias(AutoescapeBlock, 'Block', 'Escape');
+visitor(AutoescapeBlock, 'expressions');
+
+export class BlockStatement extends Node {
+ constructor(name: Identifier, body: Node) {
+ super();
+ this.name = name;
+ this.body = body;
+ }
+}
+type(BlockStatement, 'BlockStatement');
+alias(BlockStatement, 'Statement', 'Scope', 'RootScope');
+visitor(BlockStatement, 'body');
+
+export class BlockCallExpression extends Node {
+ constructor(callee: StringLiteral, args: Array = []) {
+ super();
+ this.callee = callee;
+ this.arguments = args;
+ }
+}
+type(BlockCallExpression, 'BlockCallExpression');
+alias(BlockCallExpression, 'Expression', 'FunctionInvocation');
+visitor(BlockCallExpression, 'arguments');
+
+export class MountStatement extends Node {
+ constructor(
+ name?: Identifier,
+ source?: String,
+ key?: Node,
+ argument?: Node,
+ ) {
+ super();
+ this.name = name;
+ this.source = source;
+ this.key = key;
+ this.argument = argument;
+ }
+}
+type(MountStatement, 'MountStatement');
+alias(MountStatement, 'Statement');
+visitor(MountStatement, 'name', 'source', 'key', 'argument');
+
+export class DoStatement extends Node {
+ constructor(expression: Node) {
+ super();
+ this.value = expression;
+ }
+}
+type(DoStatement, 'DoStatement');
+alias(DoStatement, 'Statement');
+visitor(DoStatement, 'value');
+
+export class EmbedStatement extends Node {
+ constructor(parent: Node) {
+ super();
+ this.parent = parent;
+ this.argument = null;
+ this.contextFree = false;
+ // when `true`, missing templates will be ignored
+ this.ignoreMissing = false;
+ this.blocks = null;
+ }
+}
+type(EmbedStatement, 'EmbedStatement');
+alias(EmbedStatement, 'Statement', 'Include');
+visitor(EmbedStatement, 'argument', 'blocks');
+
+export class ExtendsStatement extends Node {
+ constructor(parentName: Node) {
+ super();
+ this.parentName = parentName;
+ }
+}
+type(ExtendsStatement, 'ExtendsStatement');
+alias(ExtendsStatement, 'Statement', 'Include');
+visitor(ExtendsStatement, 'parentName');
+
+export class FilterBlockStatement extends Node {
+ constructor(filterExpression: Node, body: Node) {
+ super();
+ this.filterExpression = filterExpression;
+ this.body = body;
+ }
+}
+type(FilterBlockStatement, 'FilterBlockStatement');
+alias(FilterBlockStatement, 'Statement', 'Block');
+visitor(FilterBlockStatement, 'filterExpression', 'body');
+
+export class FlushStatement extends Node {
+ constructor() {
+ super();
+ }
+}
+type(FlushStatement, 'FlushStatement');
+alias(FlushStatement, 'Statement');
+
+export class ForStatement extends Node {
+ constructor(
+ keyTarget?: Identifier = null,
+ valueTarget?: Identifier = null,
+ sequence?: Node = null,
+ condition?: Node = null,
+ body?: Node = null,
+ otherwise?: Node = null,
+ ) {
+ super();
+ this.keyTarget = keyTarget;
+ this.valueTarget = valueTarget;
+ this.sequence = sequence;
+ this.condition = condition;
+ this.body = body;
+ this.otherwise = otherwise;
+ }
+}
+type(ForStatement, 'ForStatement');
+alias(ForStatement, 'Statement', 'Scope', 'Loop');
+visitor(
+ ForStatement,
+ 'keyTarget',
+ 'valueTarget',
+ 'sequence',
+ 'condition',
+ 'body',
+ 'otherwise',
+);
+
+export class ImportDeclaration extends Node {
+ constructor(key: Node, alias: Identifier) {
+ super();
+ this.key = key;
+ this.alias = alias;
+ }
+}
+type(ImportDeclaration, 'ImportDeclaration');
+alias(ImportDeclaration, 'VariableDeclaration');
+visitor(ImportDeclaration, 'key', 'value');
+
+export class FromStatement extends Node {
+ constructor(source: Node, imports: Array) {
+ super();
+ this.source = source;
+ this.imports = imports;
+ }
+}
+type(FromStatement, 'FromStatement');
+alias(FromStatement, 'Statement');
+visitor(FromStatement, 'source', 'imports');
+
+export class IfStatement extends Node {
+ constructor(test: Node, consequent?: Node = null, alternate?: Node = null) {
+ super();
+ this.test = test;
+ this.consequent = consequent;
+ this.alternate = alternate;
+ }
+}
+type(IfStatement, 'IfStatement');
+alias(IfStatement, 'Statement', 'Conditional');
+visitor(IfStatement, 'test', 'consequent', 'alternate');
+
+export class IncludeStatement extends Node {
+ constructor(source: Node) {
+ super();
+ this.source = source;
+ this.argument = null;
+ this.contextFree = false;
+ // when `true`, missing templates will be ignored
+ this.ignoreMissing = false;
+ }
+}
+type(IncludeStatement, 'IncludeStatement');
+alias(IncludeStatement, 'Statement', 'Include');
+visitor(IncludeStatement, 'source', 'argument');
+
+export class MacroDeclarationStatement extends Node {
+ constructor(name: Identifier, args: Array, body: SequenceExpression) {
+ super();
+ this.name = name;
+ this.arguments = args;
+ this.body = body;
+ }
+}
+type(MacroDeclarationStatement, 'MacroDeclarationStatement');
+alias(MacroDeclarationStatement, 'Statement', 'Scope', 'RootScope');
+visitor(MacroDeclarationStatement, 'name', 'arguments', 'body');
+
+export class VariableDeclarationStatement extends Node {
+ constructor(name: Identifier, value: Node) {
+ super();
+ this.name = name;
+ this.value = value;
+ }
+}
+type(VariableDeclarationStatement, 'VariableDeclarationStatement');
+alias(VariableDeclarationStatement, 'Statement');
+visitor(VariableDeclarationStatement, 'name', 'value');
+
+export class SetStatement extends Node {
+ constructor(assignments: Array) {
+ super();
+ this.assignments = assignments;
+ }
+}
+type(SetStatement, 'SetStatement');
+alias(SetStatement, 'Statement', 'ContextMutation');
+visitor(SetStatement, 'assignments');
+
+export class SpacelessBlock extends Node {
+ constructor(body?: Node = null) {
+ super();
+ this.body = body;
+ }
+}
+type(SpacelessBlock, 'SpacelessBlock');
+alias(SpacelessBlock, 'Statement', 'Block');
+visitor(SpacelessBlock, 'body');
+
+export class AliasExpression extends Node {
+ constructor(name: Identifier, alias: Identifier) {
+ super();
+ this.name = name;
+ this.alias = alias;
+ }
+}
+type(AliasExpression, 'AliasExpression');
+alias(AliasExpression, 'Expression');
+visitor(AliasExpression, 'name', 'alias');
+
+export class UseStatement extends Node {
+ constructor(source: Node, aliases: Array) {
+ super();
+ this.source = source;
+ this.aliases = aliases;
+ }
+}
+type(UseStatement, 'UseStatement');
+alias(UseStatement, 'Statement', 'Include');
+visitor(UseStatement, 'source', 'aliases');
+
+export {
+ UnaryNotExpression,
+ UnaryNeqExpression,
+ UnaryPosExpression,
+ BinaryOrExpression,
+ BinaryAndExpression,
+ BitwiseOrExpression,
+ BitwiseXorExpression,
+ BitwiseAndExpression,
+ BinaryEqualsExpression,
+ BinaryNotEqualsExpression,
+ BinaryLessThanExpression,
+ BinaryGreaterThanExpression,
+ BinaryLessThanOrEqualExpression,
+ BinaryGreaterThanOrEqualExpression,
+ BinaryNotInExpression,
+ BinaryInExpression,
+ BinaryMatchesExpression,
+ BinaryStartsWithExpression,
+ BinaryEndsWithExpression,
+ BinaryRangeExpression,
+ BinaryAddExpression,
+ BinaryMulExpression,
+ BinaryDivExpression,
+ BinaryFloorDivExpression,
+ BinaryModExpression,
+ BinaryPowerExpression,
+ BinaryNullCoalesceExpression,
+ TestEvenExpression,
+ TestOddExpression,
+ TestDefinedExpression,
+ TestSameAsExpression,
+ TestNullExpression,
+ TestDivisibleByExpression,
+ TestConstantExpression,
+ TestEmptyExpression,
+ TestIterableExpression,
+} from './operators';
diff --git a/packages/melody-extension-core/src/visitors/filters.js b/packages/melody-extension-core/src/visitors/filters.js
new file mode 100644
index 0000000..ae64400
--- /dev/null
+++ b/packages/melody-extension-core/src/visitors/filters.js
@@ -0,0 +1,154 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+import template from 'babel-template';
+
+// use default value if var is null, undefined or an empty string
+// but use var if value is 0, false, an empty array or an empty object
+const defaultFilter = template("VAR != null && VAR !== '' ? VAR : DEFAULT");
+
+export default {
+ capitalize: 'lodash',
+ first: 'lodash',
+ last: 'lodash',
+ keys: 'lodash',
+ default(path) {
+ // babel-template transforms it to an expression statement
+ // but we really need an expression here, so unwrap it
+ path.replaceWithJS(
+ defaultFilter({
+ VAR: path.node.target,
+ DEFAULT: path.node.arguments[0] || t.stringLiteral(''),
+ }).expression,
+ );
+ },
+ abs(path) {
+ // todo throw error if arguments exist
+ path.replaceWithJS(
+ t.callExpression(
+ t.memberExpression(t.identifier('Math'), t.identifier('abs')),
+ [path.node.target],
+ ),
+ );
+ },
+ join(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.memberExpression(path.node.target, t.identifier('join')),
+ path.node.arguments,
+ ),
+ );
+ },
+ json_encode(path) {
+ // todo: handle arguments
+ path.replaceWithJS(
+ t.callExpression(
+ t.memberExpression(
+ t.identifier('JSON'),
+ t.identifier('stringify'),
+ ),
+ [path.node.target],
+ ),
+ );
+ },
+ length(path) {
+ path.replaceWithJS(
+ t.memberExpression(path.node.target, t.identifier('length')),
+ );
+ },
+ lower(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.memberExpression(
+ path.node.target,
+ t.identifier('toLowerCase'),
+ ),
+ [],
+ ),
+ );
+ },
+ upper(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.memberExpression(
+ path.node.target,
+ t.identifier('toUpperCase'),
+ ),
+ [],
+ ),
+ );
+ },
+ slice(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.memberExpression(path.node.target, t.identifier('slice')),
+ path.node.arguments,
+ ),
+ );
+ },
+ sort(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.memberExpression(path.node.target, t.identifier('sort')),
+ path.node.arguments,
+ ),
+ );
+ },
+ split(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.memberExpression(path.node.target, t.identifier('split')),
+ path.node.arguments,
+ ),
+ );
+ },
+ trim(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.memberExpression(path.node.target, t.identifier('trim')),
+ path.node.arguments,
+ ),
+ );
+ },
+ convert_encoding(path) {
+ // encoding conversion is not supported
+ path.replaceWith(path.node.target);
+ },
+ date_modify(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.identifier(
+ path.state.addImportFrom('melody-runtime', 'strtotime'),
+ ),
+ [path.node.arguments[0], path.node.target],
+ ),
+ );
+ },
+ date(path) {
+ // Not really happy about this since moment.js could well be incompatible with
+ // the default twig behaviour
+ // might need to switch to an actual strftime implementation
+ path.repalceWithJS(
+ t.callExpression(
+ t.callExpression(
+ t.identifier(path.state.addDefaultImportFrom('moment')),
+ [path.node.target],
+ ),
+ [path.node.arguments[0]],
+ ),
+ );
+ },
+};
diff --git a/packages/melody-extension-core/src/visitors/for.js b/packages/melody-extension-core/src/visitors/for.js
new file mode 100644
index 0000000..26e08f6
--- /dev/null
+++ b/packages/melody-extension-core/src/visitors/for.js
@@ -0,0 +1,392 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { traverse } from 'melody-traverse';
+import * as t from 'babel-types';
+import babelTemplate from 'babel-template';
+
+// @param template
+// @returns function
+// @param context context bindings
+// @returns {exprStmt, initDecl, forStmt}
+const template = tpl => {
+ return ctx => parseExpr(babelTemplate(tpl)(ctx));
+};
+
+const forWithContext = template(`
+{
+let SEQUENCE = SOURCE,
+KEY_TARGET = 0,
+LENGTH = SEQUENCE.length,
+SUB_CONTEXT = CREATE_SUB_CONTEXT(CONTEXT, {
+ VALUE_TARGET: SEQUENCE[0],
+ loop: {
+ index: 1,
+ index0: 0,
+ length: LENGTH,
+ revindex: LENGTH,
+ revindex0: LENGTH - 1,
+ first: true,
+ last: 1 === LENGTH
+ }
+});
+for (;
+ KEY_TARGET < LENGTH;
+ KEY_TARGET++
+) {
+ SUB_CONTEXT.loop.index0++;
+ SUB_CONTEXT.loop.index++;
+ SUB_CONTEXT.loop.revindex--;
+ SUB_CONTEXT.loop.revindex0--;
+ SUB_CONTEXT.loop.first = false;
+ SUB_CONTEXT.loop.last = SUB_CONTEXT.loop.revindex === 0;
+ SUB_CONTEXT.VALUE_TARGET = _sequence[KEY_TARGET + 1];
+}
+}
+`);
+
+const basicFor = template(`
+{
+let SEQUENCE = SOURCE,
+KEY_TARGET = 0,
+LENGTH = SEQUENCE.length,
+VALUE_TARGET = SEQUENCE[0];
+for (;
+ KEY_TARGET < LENGTH;
+ KEY_TARGET++,
+ VALUE_TARGET = SEQUENCE[_index]
+) {
+}
+}
+`);
+
+const localFor = template(`
+{
+let SEQUENCE = SOURCE,
+KEY_TARGET = 0,
+LENGTH = SEQUENCE.length,
+VALUE_TARGET = SEQUENCE[0],
+INDEX_BY_1 = 1,
+REVERSE_INDEX_BY_1 = LENGTH,
+REVERSE_INDEX = LENGTH - 1,
+FIRST = true,
+LAST = 1 === LENGTH;
+for (;
+ KEY_TARGET < LENGTH;
+ KEY_TARGET++,
+ VALUE_TARGET = SEQUENCE[_index]
+) {
+ INDEX_BY_1++;
+ REVERSE_INDEX_BY_1--;
+ REVERSE_INDEX--;
+ FIRST = false;
+ LAST = REVERSE_INDEX === 0;
+}
+}
+`);
+
+// returns an object that has the whole expression, init declarations, for loop
+// statement in respective properties.
+function parseExpr(exprStmt) {
+ return {
+ exprStmt: exprStmt,
+ initDecl: exprStmt.body[0].declarations,
+ forStmt: exprStmt.body[1],
+ };
+}
+
+export default {
+ analyse: {
+ ForStatement: {
+ enter(path) {
+ const forStmt = path.node,
+ scope = path.scope;
+ if (forStmt.keyTarget) {
+ scope.registerBinding(
+ forStmt.keyTarget.name,
+ path.get('keyTarget'),
+ 'var',
+ );
+ }
+ if (forStmt.valueTarget) {
+ scope.registerBinding(
+ forStmt.valueTarget.name,
+ path.get('valueTarget'),
+ 'var',
+ );
+ }
+ scope.registerBinding('loop', path, 'var');
+ },
+ exit(path) {
+ const sequenceName = path.scope.generateUid('sequence'),
+ lenName = path.scope.generateUid('length');
+ path.scope.registerBinding(sequenceName, path, 'var');
+ path.scope.registerBinding(lenName, path, 'var');
+ let iName;
+ if (path.node.keyTarget) {
+ iName = path.node.keyTarget.name;
+ } else {
+ iName = path.scope.generateUid('index0');
+ path.scope.registerBinding(iName, path, 'var');
+ }
+ path.setData('forStatement.variableLookup', {
+ sequenceName,
+ lenName,
+ iName,
+ });
+
+ if (path.scope.escapesContext) {
+ const contextName = path.scope.generateUid('context');
+ path.scope.registerBinding(contextName, path, 'const');
+ path.scope.contextName = contextName;
+ path.scope.getBinding('loop').kind = 'context';
+ if (path.node.valueTarget) {
+ path.scope.getBinding(path.node.valueTarget.name).kind =
+ 'context';
+ }
+ } else if (path.scope.getBinding('loop').references) {
+ const indexName = path.scope.generateUid('index');
+ path.scope.registerBinding(indexName, path, 'var');
+ const revindexName = path.scope.generateUid('revindex');
+ path.scope.registerBinding(revindexName, path, 'var');
+ const revindex0Name = path.scope.generateUid('revindex0');
+ path.scope.registerBinding(revindex0Name, path, 'var');
+ const firstName = path.scope.generateUid('first');
+ path.scope.registerBinding(firstName, path, 'var');
+ const lastName = path.scope.generateUid('last');
+ path.scope.registerBinding(lastName, path, 'var');
+
+ const lookupTable = {
+ index: indexName,
+ index0: iName,
+ length: lenName,
+ revindex: revindexName,
+ revindex0: revindex0Name,
+ first: firstName,
+ last: lastName,
+ };
+ path.setData('forStatement.loopLookup', lookupTable);
+
+ const loopBinding = path.scope.getBinding('loop');
+ for (const loopPath of loopBinding.referencePaths) {
+ const memExpr = loopPath.parentPath;
+
+ if (memExpr.is('MemberExpression')) {
+ const typeName = memExpr.node.property.name;
+ if (typeName === 'index0') {
+ memExpr.replaceWithJS({
+ type: 'BinaryExpression',
+ operator: '-',
+ left: {
+ type: 'Identifier',
+ name: indexName,
+ },
+ right: { type: 'NumericLiteral', value: 1 },
+ extra: {
+ parenthesized: true,
+ },
+ });
+ } else {
+ memExpr.replaceWithJS({
+ type: 'Identifier',
+ name: lookupTable[typeName],
+ });
+ }
+ }
+ }
+ }
+ },
+ },
+ },
+ convert: {
+ ForStatement: {
+ enter(path) {
+ if (path.scope.escapesContext) {
+ var parentContextName = path.scope.parent.contextName;
+ if (path.node.otherwise) {
+ const alternate = path.get('otherwise');
+ if (alternate.is('Scope')) {
+ alternate.scope.contextName = parentContextName;
+ }
+ }
+
+ const sequence = path.get('sequence');
+
+ if (sequence.is('Identifier')) {
+ sequence.setData(
+ 'Identifier.contextName',
+ parentContextName,
+ );
+ } else {
+ traverse(path.node.sequence, {
+ Identifier(id) {
+ id.setData(
+ 'Identifier.contextName',
+ parentContextName,
+ );
+ },
+ });
+ }
+ }
+ },
+ exit(path) {
+ const node = path.node;
+ const { sequenceName, lenName, iName } = path.getData(
+ 'forStatement.variableLookup',
+ );
+ let expr;
+ if (path.scope.escapesContext) {
+ const contextName = path.scope.contextName;
+ expr = forWithContext({
+ CONTEXT: t.identifier(path.scope.parent.contextName),
+ SUB_CONTEXT: t.identifier(contextName),
+ CREATE_SUB_CONTEXT: t.identifier(
+ this.addImportFrom(
+ 'melody-runtime',
+ 'createSubContext',
+ ),
+ ),
+ KEY_TARGET: t.identifier(iName),
+ SOURCE: path.get('sequence').node,
+ SEQUENCE: t.identifier(sequenceName),
+ LENGTH: t.identifier(lenName),
+ VALUE_TARGET: node.valueTarget,
+ });
+ if (node.keyTarget) {
+ expr.forStmt.body.body.push({
+ type: 'ExpressionStatement',
+ expression: {
+ type: 'AssignmentExpression',
+ operator: '=',
+ left: {
+ type: 'MemberExpression',
+ object: {
+ type: 'Identifier',
+ name: contextName,
+ },
+ property: {
+ type: 'Identifier',
+ name: node.keyTarget.name,
+ },
+ computed: false,
+ },
+ right: {
+ type: 'Identifier',
+ name: iName,
+ },
+ },
+ });
+ expr.initDecl[
+ expr.initDecl.length - 1
+ ].init.arguments[1].properties.push({
+ type: 'ObjectProperty',
+ method: false,
+ shorthand: false,
+ computed: false,
+ key: {
+ type: 'Identifier',
+ name: node.keyTarget.name,
+ },
+ value: {
+ type: 'Identifier',
+ name: iName,
+ },
+ });
+ }
+ } else if (path.scope.getBinding('loop').references) {
+ const {
+ index: indexName,
+ revindex: revindexName,
+ revindex0: revindex0Name,
+ first: firstName,
+ last: lastName,
+ } = path.getData('forStatement.loopLookup');
+
+ expr = localFor({
+ KEY_TARGET: t.identifier(iName),
+ SOURCE: path.get('sequence').node,
+ SEQUENCE: t.identifier(sequenceName),
+ LENGTH: t.identifier(lenName),
+ VALUE_TARGET: node.valueTarget,
+ INDEX_BY_1: t.identifier(indexName),
+ REVERSE_INDEX: t.identifier(revindex0Name),
+ REVERSE_INDEX_BY_1: t.identifier(revindexName),
+ FIRST: t.identifier(firstName),
+ LAST: t.identifier(lastName),
+ });
+ } else {
+ expr = basicFor({
+ SEQUENCE: t.identifier(sequenceName),
+ SOURCE: path.get('sequence').node,
+ KEY_TARGET: t.identifier(iName),
+ LENGTH: t.identifier(lenName),
+ VALUE_TARGET: node.valueTarget,
+ });
+ }
+
+ expr.forStmt.body.body.unshift(...path.get('body').node.body);
+
+ let uniteratedName;
+ if (node.otherwise) {
+ uniteratedName = path.scope.generateUid('uniterated');
+ path.scope.parent.registerBinding(
+ uniteratedName,
+ path,
+ 'var',
+ );
+ expr.forStmt.body.body.push(
+ t.expressionStatement(
+ t.assignmentExpression(
+ '=',
+ t.identifier(uniteratedName),
+ t.booleanLiteral(false),
+ ),
+ ),
+ );
+ }
+
+ if (node.condition) {
+ expr.forStmt.body = t.blockStatement([
+ {
+ type: 'IfStatement',
+ test: node.condition,
+ consequent: t.blockStatement(
+ expr.forStmt.body.body,
+ ),
+ },
+ ]);
+ }
+
+ if (uniteratedName) {
+ path.replaceWithMultipleJS(
+ t.variableDeclaration('let', [
+ t.variableDeclarator(
+ t.identifier(uniteratedName),
+ t.booleanLiteral(true),
+ ),
+ ]),
+ expr.exprStmt,
+ t.ifStatement(
+ t.identifier(uniteratedName),
+ node.otherwise,
+ ),
+ );
+ } else {
+ path.replaceWithJS(expr.exprStmt);
+ }
+ },
+ },
+ },
+};
diff --git a/packages/melody-extension-core/src/visitors/functions.js b/packages/melody-extension-core/src/visitors/functions.js
new file mode 100644
index 0000000..c701cf9
--- /dev/null
+++ b/packages/melody-extension-core/src/visitors/functions.js
@@ -0,0 +1,100 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+
+function addOne(expr) {
+ return t.binaryExpression('+', expr, t.numericLiteral(1));
+}
+
+export default {
+ range(path) {
+ const args = path.node.arguments;
+ const callArgs = [];
+ if (args.length === 1) {
+ callArgs.push(addOne(args[0]));
+ } else if (args.length === 3) {
+ callArgs.push(args[0]);
+ callArgs.push(addOne(args[1]));
+ callArgs.push(args[2]);
+ } else if (args.length === 2) {
+ callArgs.push(args[0], addOne(args[1]));
+ } else {
+ path.state.error(
+ 'Invalid range call',
+ path.node.pos,
+ `The range function accepts 1 to 3 arguments but you have specified ${args.length} arguments instead.`,
+ );
+ }
+
+ path.replaceWithJS(
+ t.callExpression(
+ t.identifier(path.state.addImportFrom('lodash', 'range')),
+ callArgs,
+ ),
+ );
+ },
+ // range: 'lodash',
+ dump(path) {
+ if (!path.parentPath.is('PrintExpressionStatement')) {
+ path.state.error(
+ 'dump must be used in a lone expression',
+ path.node.pos,
+ 'The dump function does not have a return value. Thus it must be used as the only expression.',
+ );
+ }
+ path.parentPath.replaceWithJS(
+ t.expressionStatement(
+ t.callExpression(
+ t.memberExpression(
+ t.identifier('console'),
+ t.identifier('log'),
+ ),
+ path.node.arguments,
+ ),
+ ),
+ );
+ },
+ include(path) {
+ if (!path.parentPath.is('PrintExpressionStatement')) {
+ path.state.error({
+ title: 'Include function does not return value',
+ pos: path.node.loc.start,
+ advice: `The include function currently does not return a value.
+ Thus you must use it like a regular include tag.`,
+ });
+ }
+ const includeName = path.scope.generateUid('include');
+ const importDecl = t.importDeclaration(
+ [t.importDefaultSpecifier(t.identifier(includeName))],
+ path.node.arguments[0],
+ );
+ path.state.program.body.splice(0, 0, importDecl);
+ path.scope.registerBinding(includeName);
+
+ const argument = path.node.arguments[1];
+
+ const includeCall = t.expressionStatement(
+ t.callExpression(
+ t.memberExpression(
+ t.identifier(includeName),
+ t.identifier('render'),
+ ),
+ argument ? [argument] : [],
+ ),
+ );
+ path.replaceWithJS(includeCall);
+ },
+};
diff --git a/packages/melody-extension-core/src/visitors/tests.js b/packages/melody-extension-core/src/visitors/tests.js
new file mode 100644
index 0000000..f5359c1
--- /dev/null
+++ b/packages/melody-extension-core/src/visitors/tests.js
@@ -0,0 +1,127 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+
+export default {
+ convert: {
+ TestEvenExpression: {
+ exit(path) {
+ const expr = t.unaryExpression(
+ '!',
+ t.binaryExpression(
+ '%',
+ path.get('expression').node,
+ t.numericLiteral(2),
+ ),
+ );
+ expr.extra = { parenthesizedArgument: true };
+ path.replaceWithJS(expr);
+ },
+ },
+ TestOddExpression: {
+ exit(path) {
+ const expr = t.unaryExpression(
+ '!',
+ t.unaryExpression(
+ '!',
+ t.binaryExpression(
+ '%',
+ path.get('expression').node,
+ t.numericLiteral(2),
+ ),
+ ),
+ );
+ expr.extra = { parenthesizedArgument: true };
+ path.replaceWithJS(expr);
+ },
+ },
+ TestDefinedExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.binaryExpression(
+ '!==',
+ t.unaryExpression(
+ 'typeof',
+ path.get('expression').node,
+ ),
+ t.stringLiteral('undefined'),
+ ),
+ );
+ },
+ },
+ TestEmptyExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.identifier(
+ this.addImportFrom('melody-runtime', 'isEmpty'),
+ ),
+ [path.get('expression').node],
+ ),
+ );
+ },
+ },
+ TestSameAsExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.binaryExpression(
+ '===',
+ path.get('expression').node,
+ path.get('arguments')[0].node,
+ ),
+ );
+ },
+ },
+ TestNullExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.binaryExpression(
+ '===',
+ path.get('expression').node,
+ t.nullLiteral(),
+ ),
+ );
+ },
+ },
+ TestDivisibleByExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.unaryExpression(
+ '!',
+ t.binaryExpression(
+ '%',
+ path.get('expression').node,
+ path.node.arguments[0],
+ ),
+ ),
+ );
+ },
+ },
+ TestIterableExpression: {
+ exit(path) {
+ path.replaceWithJS(
+ t.callExpression(
+ t.memberExpression(
+ t.identifier('Array'),
+ t.identifier('isArray'),
+ ),
+ [path.node.expression],
+ ),
+ );
+ },
+ },
+ },
+};
diff --git a/packages/melody-hoc/__tests__/BindEventsSpec.js b/packages/melody-hoc/__tests__/BindEventsSpec.js
new file mode 100644
index 0000000..8412b00
--- /dev/null
+++ b/packages/melody-hoc/__tests__/BindEventsSpec.js
@@ -0,0 +1,103 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { expect } from 'chai';
+
+import { createComponent, render } from 'melody-component';
+import {
+ patchOuter,
+ elementOpen,
+ elementClose,
+ component,
+ ref,
+ flush,
+} from 'melody-idom';
+import { bindEvents, lifecycle, compose } from '../src';
+
+const template = {
+ render(_context) {
+ elementOpen('div', null, ['ref', _context.div]);
+ elementClose('div');
+ },
+};
+
+function dispatchClick(el) {
+ const event = document.createEvent('MouseEvents');
+ event.initEvent('click', true, true);
+ el.dispatchEvent(event);
+}
+
+describe('BindEvents', function() {
+ it('should bind event handlers when mounted', function() {
+ const root = document.createElement('div');
+ let clicked = false;
+ let context;
+ const enhance = bindEvents({
+ div: {
+ click(event, component) {
+ clicked = true;
+ context = component;
+ },
+ },
+ });
+
+ const MyComponent = createComponent(template);
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, {});
+ dispatchClick(root);
+ expect(clicked).to.equal(true);
+ expect(context).to.be.a('object');
+ expect(context.props).to.be.a('object');
+ });
+ it('should unbind event handlers when unmounted', function() {
+ const root = document.createElement('div');
+ let clickedCount = 0;
+
+ const enhance = compose(
+ bindEvents({
+ div: {
+ click() {
+ clickedCount++;
+ },
+ },
+ }),
+ );
+
+ const MyComponent = createComponent(template);
+ const EnhancedComponent = enhance(MyComponent);
+
+ const renderTemplate = _context => {
+ elementOpen('div');
+ if (_context.comp) {
+ component(EnhancedComponent, 'test');
+ }
+ elementClose('div');
+ };
+
+ patchOuter(root, renderTemplate, { comp: true });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ const croot = root.childNodes[0];
+ dispatchClick(croot);
+ patchOuter(root, renderTemplate, { comp: false });
+ dispatchClick(croot);
+ expect(clickedCount).to.equal(1);
+ });
+});
diff --git a/packages/melody-hoc/__tests__/DefaultPropsSpec.js b/packages/melody-hoc/__tests__/DefaultPropsSpec.js
new file mode 100644
index 0000000..0356480
--- /dev/null
+++ b/packages/melody-hoc/__tests__/DefaultPropsSpec.js
@@ -0,0 +1,91 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { expect } from 'chai';
+
+import { createComponent, render } from 'melody-component';
+import { elementOpen, elementClose } from 'melody-idom';
+import { defaultProps } from '../src';
+
+const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ elementClose('div');
+ },
+};
+
+describe('DefaultProps', function() {
+ it('should fill missing properties', function() {
+ const root = document.createElement('div');
+ let loggedProps;
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ loggedProps = this.props;
+ },
+ });
+
+ const enhance = defaultProps({ foo: 'bar', qux: 'qax' });
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'baz' });
+ expect(loggedProps).to.deep.equal({ qux: 'qax', foo: 'baz' });
+ });
+ it('should fill undefined properties', function() {
+ const root = document.createElement('div');
+ let loggedProps;
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ loggedProps = this.props;
+ },
+ });
+
+ const enhance = defaultProps({ foo: 'bar', qux: 'qax' });
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'baz', qux: undefined });
+ expect(loggedProps).to.deep.equal({ qux: 'qax', foo: 'baz' });
+ });
+ it('should not fill null properties', function() {
+ const root = document.createElement('div');
+ let loggedProps;
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ loggedProps = this.props;
+ },
+ });
+
+ const enhance = defaultProps({ foo: 'bar', qux: 'qax' });
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'baz', qux: null });
+ expect(loggedProps).to.deep.equal({ qux: null, foo: 'baz' });
+ });
+ it('should fill missing properties on update', function() {
+ const root = document.createElement('div');
+ let loggedProps;
+ const MyComponent = createComponent(template, undefined, {
+ componentDidUpdate() {
+ loggedProps = this.props;
+ },
+ });
+
+ const enhance = defaultProps({ foo: 'bar', qux: 'qax' });
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'baz' });
+ render(root, EnhancedComponent, {});
+ expect(loggedProps).to.deep.equal({ foo: 'bar', qux: 'qax' });
+ });
+});
diff --git a/packages/melody-hoc/__tests__/LifeCycleSpec.js b/packages/melody-hoc/__tests__/LifeCycleSpec.js
new file mode 100644
index 0000000..142f778
--- /dev/null
+++ b/packages/melody-hoc/__tests__/LifeCycleSpec.js
@@ -0,0 +1,220 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createComponent, render } from 'melody-component';
+import {
+ patchOuter,
+ elementOpen,
+ elementClose,
+ ref,
+ component,
+ flush,
+} from 'melody-idom';
+import { compose, lifecycle } from '../src';
+
+const template = {
+ render(_context) {
+ ref('bar', elementOpen('div', null));
+ elementClose('div');
+ },
+};
+
+describe('LifeCycle', function() {
+ it('should call lifecycle methods in the correct order', function() {
+ const root = document.createElement('div');
+ const log = [];
+
+ const enhance = compose(
+ lifecycle({
+ componentDidInitialize() {
+ log.push(2);
+ },
+ }),
+ lifecycle({
+ componentDidInitialize() {
+ log.push(1);
+ },
+ }),
+ );
+
+ const MyComponent = createComponent(template, undefined, {
+ componentDidInitialize() {
+ log.push(0);
+ },
+ });
+
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'bar' });
+ expect(log).toEqual([0, 1, 2]);
+ });
+ it('should support all lifecycle methods', function() {
+ const root = document.createElement('div');
+ const log = [];
+
+ const enhance = lifecycle({
+ componentDidInitialize() {
+ log.push('componentDidInitialize');
+ },
+ componentWillMount() {
+ log.push('componentWillMount');
+ },
+ componentDidMount() {
+ log.push('componentDidMount');
+ },
+ componentWillUpdate() {
+ log.push('componentWillUpdate');
+ },
+ componentDidUpdate() {
+ log.push('componentDidUpdate');
+ },
+ componentWillUnmount() {
+ log.push('componentWillUnmount');
+ },
+ });
+
+ const MyComponent = createComponent(template);
+ const EnhancedComponent = enhance(MyComponent);
+
+ const renderTemplate = _context => {
+ elementOpen('div');
+ if (_context.comp) {
+ component(EnhancedComponent, 'test', { foo: _context.foo });
+ }
+ elementClose('div');
+ };
+
+ patchOuter(root, renderTemplate, { comp: true, foo: 'bar' });
+ finishRendering();
+ patchOuter(root, renderTemplate, { comp: true, foo: 'qux' });
+ finishRendering();
+ patchOuter(root, renderTemplate, { comp: false });
+ finishRendering();
+ expect(log).toEqual([
+ 'componentDidInitialize',
+ 'componentWillMount',
+ 'componentDidMount',
+ 'componentWillUpdate',
+ 'componentDidUpdate',
+ 'componentWillUnmount',
+ ]);
+ });
+ it('should bind lifecycle methods to an own context', function() {
+ const root = document.createElement('div');
+ let lifecycleContext;
+ let componentContext;
+
+ const enhance = lifecycle({
+ componentDidInitialize() {
+ lifecycleContext = this;
+ },
+ });
+
+ const MyComponent = createComponent(template, undefined, {
+ componentDidInitialize() {
+ componentContext = this;
+ },
+ });
+
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'bar' });
+ expect(lifecycleContext).not.toEqual(componentContext);
+ });
+ describe('lifecycle context', function() {
+ it('should have access to important component properties', function() {
+ const root = document.createElement('div');
+ let lifecycleContext;
+ let componentContext;
+
+ const enhance = lifecycle({
+ componentDidMount() {
+ lifecycleContext = this;
+ },
+ });
+
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ componentContext = this;
+ },
+ });
+ const EnhancedComponent = enhance(MyComponent);
+ render(root, EnhancedComponent, { foo: 'bar' });
+ expect(lifecycleContext.el).toEqual(componentContext.el);
+ expect(lifecycleContext.refs).toEqual(componentContext.refs);
+ expect(lifecycleContext.props).toEqual(componentContext.props);
+ expect(lifecycleContext.state).toEqual(componentContext.state);
+ expect(lifecycleContext.dispatch).toBeInstanceOf(Function);
+ expect(lifecycleContext.getState).toBeInstanceOf(Function);
+ });
+ it('should be the same context through all lifecycles', function() {
+ const root = document.createElement('div');
+ const contexts = [];
+
+ const enhance = lifecycle({
+ componentDidInitialize() {
+ contexts.push(this);
+ },
+ componentWillMount() {
+ contexts.push(this);
+ },
+ componentDidMount() {
+ contexts.push(this);
+ },
+ componentWillUpdate() {
+ contexts.push(this);
+ },
+ componentDidUpdate() {
+ contexts.push(this);
+ },
+ componentWillUnmount() {
+ contexts.push(this);
+ },
+ });
+
+ const MyComponent = createComponent(template);
+ const EnhancedComponent = enhance(MyComponent);
+
+ const renderTemplate = _context => {
+ elementOpen('div');
+ if (_context.comp) {
+ component(EnhancedComponent, 'test', { foo: _context.foo });
+ }
+ elementClose('div');
+ };
+
+ patchOuter(root, renderTemplate, { comp: true, foo: 'bar' });
+ finishRendering();
+ patchOuter(root, renderTemplate, { comp: true, foo: 'qux' });
+ finishRendering();
+ patchOuter(root, renderTemplate, { comp: false });
+ finishRendering();
+ expect(contexts[1]).toEqual(contexts[0]);
+ expect(contexts[2]).toEqual(contexts[0]);
+ expect(contexts[3]).toEqual(contexts[0]);
+ expect(contexts[4]).toEqual(contexts[0]);
+ expect(contexts[5]).toEqual(contexts[0]);
+ });
+ });
+});
+
+function finishRendering() {
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+}
diff --git a/packages/melody-hoc/__tests__/MapPropsSpec.js b/packages/melody-hoc/__tests__/MapPropsSpec.js
new file mode 100644
index 0000000..44b1867
--- /dev/null
+++ b/packages/melody-hoc/__tests__/MapPropsSpec.js
@@ -0,0 +1,82 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { expect } from 'chai';
+
+import { createComponent, render } from 'melody-component';
+import { elementOpen, elementClose } from 'melody-idom';
+import { mapProps } from '../src';
+
+const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ elementClose('div');
+ },
+};
+
+describe('MapProps', function() {
+ it('should map properties', function() {
+ const root = document.createElement('div');
+ let loggedProps;
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ loggedProps = this.props;
+ },
+ });
+
+ const enhance = mapProps(props => ({ ...props, qux: 'qax' }));
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'bar' });
+ expect(loggedProps).to.deep.equal({ qux: 'qax', foo: 'bar' });
+ });
+ it('should map properties on update', function() {
+ const root = document.createElement('div');
+ let loggedProps;
+ const MyComponent = createComponent(template, undefined, {
+ componentDidUpdate() {
+ loggedProps = this.props;
+ },
+ });
+
+ const enhance = mapProps(props => ({ ...props, qux: 'qax' }));
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'bar' });
+ render(root, EnhancedComponent, { fux: 'bax' });
+ expect(loggedProps).to.deep.equal({ qux: 'qax', fux: 'bax' });
+ });
+ it('should be possible to use multiple mapProps hoc', () => {
+ const root = document.createElement('div');
+ let loggedProps;
+ const MyComponent = createComponent(template, undefined, {
+ componentDidUpdate() {
+ loggedProps = this.props;
+ },
+ });
+
+ const enhance = mapProps(props => ({ ...props, qux: 'qax' }));
+ const enhance2 = mapProps(props => ({ ...props, bux: 'bax' }));
+ const EnhancedComponent = enhance2(enhance(MyComponent));
+
+ render(root, EnhancedComponent, { foo: 'bar' });
+ render(root, EnhancedComponent, { fux: 'bax' });
+ expect(loggedProps).to.deep.equal({
+ qux: 'qax',
+ fux: 'bax',
+ bux: 'bax',
+ });
+ });
+});
diff --git a/packages/melody-hoc/__tests__/WithHandlersSpec.js b/packages/melody-hoc/__tests__/WithHandlersSpec.js
new file mode 100644
index 0000000..0937088
--- /dev/null
+++ b/packages/melody-hoc/__tests__/WithHandlersSpec.js
@@ -0,0 +1,170 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createComponent, render } from 'melody-component';
+import { elementOpen, elementClose, elementVoid } from 'melody-idom';
+import withHandlers from '../src/withHandlers';
+
+const template = {
+ render(props) {
+ elementOpen('button', null, ['onclick', props.onClick]);
+ elementVoid('input', null, ['onchange', props.onChange]);
+ elementClose('button');
+ },
+};
+
+describe('WithHandlers', () => {
+ it('should map handlers', () => {
+ const root = document.createElement('button');
+ let props = null;
+
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ props = this.props;
+ },
+ });
+
+ const eventResults = {};
+
+ const enhance = withHandlers({
+ onClick: props => event => {
+ eventResults.onClick = { props, event };
+ },
+ onChange: props => event => {
+ eventResults.onChange = { props, event };
+ },
+ });
+
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'bar' });
+
+ expect(props.onClick).toBeInstanceOf(Function);
+ expect(props.onChange).toBeInstanceOf(Function);
+
+ const assertProps = (handler, fooValue, event) => {
+ props[handler](event);
+
+ const r = eventResults[handler];
+ expect(r.event).toEqual(event);
+ expect(r.props).toBeTruthy();
+ expect(r.props.foo).toEqual(fooValue);
+ };
+
+ assertProps('onClick', 'bar', { type: 'click', id: 0 });
+ assertProps('onChange', 'bar', { type: 'change', id: 1 });
+
+ render(root, EnhancedComponent, { foo: 'quu' });
+
+ assertProps('onClick', 'quu', { type: 'click', id: 2 });
+ assertProps('onChange', 'quu', { type: 'change', id: 3 });
+ });
+
+ it('should not modify handler props', () => {
+ const root = document.createElement('button');
+ let props = null;
+
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ props = this.props;
+ },
+ });
+
+ const enhance = withHandlers({
+ handler: props => event => null,
+ });
+
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'bar' });
+ const firstProps = props;
+
+ render(root, EnhancedComponent, { foo: 'bar' });
+ const secondProps = props;
+
+ expect(firstProps.handler).toBeTruthy();
+ expect(firstProps.handler).toEqual(secondProps.handler);
+ });
+
+ it('should cache handlers', () => {
+ const root = document.createElement('button');
+ let props = null;
+
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ props = this.props;
+ },
+ });
+
+ let count = 0;
+ let triggerCount = 0;
+ const enhance = withHandlers({
+ handler: props => {
+ count++;
+ return event => triggerCount++;
+ },
+ });
+
+ const EnhancedComponent = enhance(MyComponent);
+
+ // Don't create handler until it is called
+ render(root, EnhancedComponent, { foo: 'bar' });
+ expect(count).toEqual(0);
+ expect(triggerCount).toEqual(0);
+
+ props.handler();
+ expect(count).toEqual(1);
+ expect(triggerCount).toEqual(1);
+
+ // Props haven't changed; should use cached handler
+ props.handler();
+ expect(count).toEqual(1);
+ expect(triggerCount).toEqual(2);
+
+ render(root, EnhancedComponent, { foo: 'quu' });
+ props.handler();
+ // Props did change; handler should be recreated
+ expect(count).toEqual(2);
+ expect(triggerCount).toEqual(3);
+ });
+
+ it('should warn if handler is not a higher order function', () => {
+ const root = document.createElement('button');
+ let props = null;
+
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ props = this.props;
+ },
+ });
+
+ const oldError = console.error;
+ console.error = jest.fn();
+
+ const enhance = withHandlers({
+ handler: props => {},
+ });
+
+ const EnhancedComponent = enhance(MyComponent);
+
+ // Don't create handler until it is called
+ render(root, EnhancedComponent, { foo: 'bar' });
+ expect(() => props.handler()).toThrow();
+ expect(console.error).toHaveBeenCalledWith(
+ 'withHandlers(): Expected a map of higher-order functions. Refer to the docs for more info.',
+ );
+ console.error = oldError;
+ });
+});
diff --git a/packages/melody-hoc/__tests__/WithPropsSpec.js b/packages/melody-hoc/__tests__/WithPropsSpec.js
new file mode 100644
index 0000000..37fbcfe
--- /dev/null
+++ b/packages/melody-hoc/__tests__/WithPropsSpec.js
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { expect } from 'chai';
+
+import { createComponent, render } from 'melody-component';
+import { elementOpen, elementClose } from 'melody-idom';
+import { withProps } from '../src';
+
+const template = {
+ render(_context) {
+ elementOpen('div', null, null);
+ elementClose('div');
+ },
+};
+
+describe('DefaultProps', function() {
+ it('should add properties', function() {
+ const root = document.createElement('div');
+ let loggedProps;
+ const MyComponent = createComponent(template, undefined, {
+ componentDidMount() {
+ loggedProps = this.props;
+ },
+ });
+
+ const enhance = withProps({ foo: 'bar', qux: 'qax' });
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'baz' });
+ expect(loggedProps).to.deep.equal({ foo: 'bar', qux: 'qax' });
+ });
+ it('should add properties on update', function() {
+ const root = document.createElement('div');
+ let loggedProps;
+ const MyComponent = createComponent(template, undefined, {
+ componentDidUpdate() {
+ loggedProps = this.props;
+ },
+ });
+
+ const enhance = withProps({ foo: 'bar', qux: 'qax' });
+ const EnhancedComponent = enhance(MyComponent);
+
+ render(root, EnhancedComponent, { foo: 'baz' });
+ render(root, EnhancedComponent, { doo: 'woo' });
+ expect(loggedProps).to.deep.equal({
+ foo: 'bar',
+ qux: 'qax',
+ doo: 'woo',
+ });
+ });
+});
diff --git a/packages/melody-hoc/__tests__/WithRefsSpec.js b/packages/melody-hoc/__tests__/WithRefsSpec.js
new file mode 100644
index 0000000..ad6ddce
--- /dev/null
+++ b/packages/melody-hoc/__tests__/WithRefsSpec.js
@@ -0,0 +1,99 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import template from './__fixtures__/withRefs.twig';
+import { createComponent, render } from 'melody-component';
+import { withRefs, bindEvents } from '../src';
+
+describe('withRefs', () => {
+ it('renders the component', () => {
+ const getRefs = jest.fn(() => ({ unsubscribe: jest.fn() }));
+ const enhance = withRefs(getRefs);
+ const Component = enhance(createComponent(template));
+ const el = document.createElement('div');
+ render(el, Component, {
+ text: 'foo',
+ });
+ expect(el.outerHTML).toMatchSnapshot();
+ });
+
+ it('invokes the ref retrieval function', () => {
+ const getRefs = jest.fn(() => ({ unsubscribe: jest.fn() }));
+ const enhance = withRefs(getRefs);
+ const Component = enhance(createComponent(template));
+ const el = document.createElement('div');
+ render(el, Component, {
+ text: 'foo',
+ });
+ expect(getRefs).toHaveBeenCalled();
+ });
+
+ it('registers the ref', () => {
+ const myRef = jest.fn(() => ({ unsubscribe: jest.fn() }));
+ const enhance = withRefs(() => ({ myRef }));
+ const Component = enhance(createComponent(template));
+ const el = document.createElement('div');
+ render(el, Component, {
+ text: 'foo',
+ });
+ expect(myRef).toHaveBeenCalledWith(el.querySelector('a'));
+ });
+
+ it('passes the component to the ref handler', () => {
+ let text = '';
+ const enhance = withRefs(component => {
+ return {
+ myRef: el => {
+ const handler = () => (text = component.getState().text);
+ el.addEventListener('click', handler);
+ return {
+ unsubscribe() {
+ el.removeEventListener('click', handler);
+ },
+ };
+ },
+ };
+ });
+ const Component = enhance(createComponent(template));
+ const el = document.createElement('div');
+ render(el, Component, {
+ text: 'bar',
+ });
+ el
+ .querySelector('a')
+ .dispatchEvent(new Event('click', { bubbles: true }));
+ expect(text).toEqual('bar');
+ });
+
+ it('allows the creation of a nice API for binding event handlers', () => {
+ let text = '';
+ const enhance = bindEvents({
+ myRef: {
+ click(event, component) {
+ text = component.getState().text;
+ },
+ },
+ });
+ const Component = enhance(createComponent(template));
+ const el = document.createElement('div');
+ render(el, Component, {
+ text: 'bar',
+ });
+ el
+ .querySelector('a')
+ .dispatchEvent(new Event('click', { bubbles: true }));
+ expect(text).toEqual('bar');
+ });
+});
diff --git a/packages/melody-hoc/__tests__/WithStoreSpec.js b/packages/melody-hoc/__tests__/WithStoreSpec.js
new file mode 100644
index 0000000..ee5659d
--- /dev/null
+++ b/packages/melody-hoc/__tests__/WithStoreSpec.js
@@ -0,0 +1,192 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createComponent, render } from 'melody-component';
+import {
+ elementOpen,
+ elementClose,
+ text,
+ flush as _flush,
+ component,
+} from 'melody-idom';
+import withStore, { RECEIVE_PROPS } from '../src/withStore';
+import { createStore } from 'redux';
+
+const template = {
+ render({ name, state, dispatch }) {
+ elementOpen('button', null, [
+ 'onclick',
+ () => dispatch({ type: 'INC' }),
+ ]);
+ text(name);
+ text(' ');
+ text(state);
+ elementClose('button');
+ },
+};
+
+const flush = () => {
+ _flush({
+ didTimeout: true,
+ timeRemaining() {
+ return 0;
+ },
+ });
+};
+
+describe('WithStore', () => {
+ let store;
+ let lastProps;
+ let StatelessComp;
+ let StatefulComp;
+ let root;
+
+ beforeEach(() => {
+ store = createStore((state = 0, { type, payload }) => {
+ switch (type) {
+ case RECEIVE_PROPS:
+ lastProps = payload;
+ return state;
+ case 'INC':
+ return state + 1;
+ }
+ });
+
+ lastProps = undefined;
+ StatelessComp = createComponent(template);
+ StatefulComp = withStore(() => store)(StatelessComp);
+ root = document.createElement('button');
+ });
+
+ it('should map state to props', () => {
+ render(root, StatefulComp, { name: 'foo' });
+
+ expect(root.outerHTML).toEqual('foo 0 ');
+ });
+
+ it('should rerender when store updates', () => {
+ render(root, StatefulComp, { name: 'foo' });
+
+ expect(root.outerHTML).toEqual('foo 0 ');
+
+ store.dispatch({ type: 'INC' });
+ flush();
+
+ expect(root.outerHTML).toEqual('foo 1 ');
+
+ store.dispatch({ type: 'INC' });
+ flush();
+
+ expect(root.outerHTML).toEqual('foo 2 ');
+
+ render(root, StatefulComp, { name: 'bar' });
+ expect(root.outerHTML).toEqual('bar 2 ');
+ });
+
+ it('should map dispatch to props', () => {
+ render(root, StatefulComp, { name: 'foo' });
+
+ expect(root.outerHTML).toEqual('foo 0 ');
+
+ root.click();
+ flush();
+
+ expect(root.outerHTML).toEqual('foo 1 ');
+
+ root.click();
+ flush();
+
+ expect(root.outerHTML).toEqual('foo 2 ');
+ });
+
+ it('should unsubscribe from store on unmount', () => {
+ let unsubCount = 0;
+ const replaceSubscribe = () => {
+ const oldSub = store.subscribe;
+ store.subscribe = listener => {
+ const unsub = oldSub(listener);
+ return () => {
+ unsubCount++;
+ unsub();
+ };
+ };
+ };
+
+ replaceSubscribe();
+
+ const Wrapper = createComponent({
+ render(props) {
+ elementOpen('div');
+ if (!props.hide) {
+ component(StatefulComp, 'comp', { name: 'foo' });
+ }
+ elementClose('div');
+ },
+ });
+
+ const wrapperRoot = document.createElement('div');
+
+ render(wrapperRoot, Wrapper, { hide: false });
+ expect(unsubCount).toBe(0);
+ render(wrapperRoot, Wrapper, { hide: true });
+ expect(unsubCount).toBe(1);
+ });
+
+ it('should subscribe to store after unmounting/mounting', () => {
+ let subCount = 0;
+ const replaceSubscribe = () => {
+ const oldSub = store.subscribe;
+ store.subscribe = listener => {
+ subCount++;
+ return oldSub(listener);
+ };
+ };
+
+ replaceSubscribe();
+
+ const Wrapper = createComponent({
+ render(props) {
+ elementOpen('div');
+ if (!props.hide) {
+ component(StatefulComp, 'comp', { name: 'foo' });
+ }
+ elementClose('div');
+ },
+ });
+
+ const wrapperRoot = document.createElement('div');
+
+ render(wrapperRoot, Wrapper, { hide: false });
+ expect(subCount).toBe(1);
+ render(wrapperRoot, Wrapper, { hide: true });
+ expect(subCount).toBe(1);
+ render(wrapperRoot, Wrapper, { hide: false });
+ expect(subCount).toBe(2);
+ });
+
+ it('should receive props', () => {
+ expect(lastProps).toBe(undefined);
+
+ render(root, StatefulComp, { name: 'foo' });
+ expect(lastProps).toEqual({ name: 'foo' });
+
+ render(root, StatefulComp, { name: 'bar' });
+ expect(lastProps).toEqual({ name: 'bar' });
+
+ store.dispatch({ type: 'INC' });
+ flush();
+ expect(lastProps).toEqual({ name: 'bar' });
+ });
+});
diff --git a/packages/melody-hoc/__tests__/__fixtures__/withRefs.twig b/packages/melody-hoc/__tests__/__fixtures__/withRefs.twig
new file mode 100644
index 0000000..816affa
--- /dev/null
+++ b/packages/melody-hoc/__tests__/__fixtures__/withRefs.twig
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/packages/melody-hoc/__tests__/__snapshots__/WithRefsSpec.js.snap b/packages/melody-hoc/__tests__/__snapshots__/WithRefsSpec.js.snap
new file mode 100644
index 0000000..f18d36c
--- /dev/null
+++ b/packages/melody-hoc/__tests__/__snapshots__/WithRefsSpec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`withRefs renders the component 1`] = `""`;
diff --git a/packages/melody-hoc/package.json b/packages/melody-hoc/package.json
new file mode 100644
index 0000000..8529c58
--- /dev/null
+++ b/packages/melody-hoc/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "melody-hoc",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash": "^4.15.0",
+ "melody-component": "^0.11.1-rc.1"
+ },
+ "peerDependencies": {
+ "melody-idom": "^0.11.1-rc.1"
+ },
+ "devDependencies": {
+ "melody-idom": "^0.11.1-rc.1",
+ "melody-test-utils": "^0.11.1-rc.1",
+ "redux": "^3.6.0"
+ }
+}
diff --git a/packages/melody-hoc/src/bindEvents.js b/packages/melody-hoc/src/bindEvents.js
new file mode 100644
index 0000000..dcb152d
--- /dev/null
+++ b/packages/melody-hoc/src/bindEvents.js
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import withRefs from './withRefs';
+
+export default function bindEvents(map) {
+ return withRefs(component => {
+ return Object.keys(map).reduce((acc, refName) => {
+ const eventMap = map[refName];
+ acc[refName] = el => {
+ const unsubscribers = Object.keys(eventMap).map(eventName => {
+ const _handler = eventMap[eventName];
+ const handler = event => _handler(event, component);
+ el.addEventListener(eventName, handler, false);
+ return () =>
+ el.removeEventListener(eventName, handler, false);
+ });
+ return {
+ unsubscribe() {
+ unsubscribers.forEach(f => f());
+ unsubscribers.length = 0;
+ },
+ };
+ };
+ return acc;
+ }, {});
+ });
+}
diff --git a/packages/melody-hoc/src/compose.js b/packages/melody-hoc/src/compose.js
new file mode 100644
index 0000000..4e7b8cb
--- /dev/null
+++ b/packages/melody-hoc/src/compose.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { flowRight } from 'lodash';
+export default flowRight;
diff --git a/packages/melody-hoc/src/defaultProps.js b/packages/melody-hoc/src/defaultProps.js
new file mode 100644
index 0000000..9cb130e
--- /dev/null
+++ b/packages/melody-hoc/src/defaultProps.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import mapProps from './mapProps';
+import { defaults } from 'lodash';
+
+/**
+ * Specifies props to be passed by default to the base component.
+ * Similar to withProps(), except the props from the owner take
+ * precedence over props provided to the Hoc.
+ */
+export default function defaultProps(defaultProps) {
+ return mapProps(props => defaults({}, props, defaultProps));
+}
diff --git a/packages/melody-hoc/src/index.js b/packages/melody-hoc/src/index.js
new file mode 100644
index 0000000..492d84d
--- /dev/null
+++ b/packages/melody-hoc/src/index.js
@@ -0,0 +1,35 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Utility
+import _compose from './compose';
+export const compose = _compose;
+
+import _lifecycle from './lifecycle';
+export const lifecycle = _lifecycle;
+import _bindEvents from './bindEvents';
+export const bindEvents = _bindEvents;
+import _mapProps from './mapProps';
+export const mapProps = _mapProps;
+import _defaultProps from './defaultProps';
+export const defaultProps = _defaultProps;
+import _withProps from './withProps';
+export const withProps = _withProps;
+import _withHandlers from './withHandlers';
+export const withHandlers = _withHandlers;
+import _withStore from './withStore';
+export const withStore = _withStore;
+import _withRefs from './withRefs';
+export const withRefs = _withRefs;
diff --git a/packages/melody-hoc/src/lifecycle.js b/packages/melody-hoc/src/lifecycle.js
new file mode 100644
index 0000000..5e7c975
--- /dev/null
+++ b/packages/melody-hoc/src/lifecycle.js
@@ -0,0 +1,99 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const createScope = component => ({
+ get el() {
+ return component.el;
+ },
+ get refs() {
+ return component.refs;
+ },
+ get props() {
+ return component.props;
+ },
+ get state() {
+ return component.state;
+ },
+ dispatch(action) {
+ return component.dispatch(action);
+ },
+ getState() {
+ return component.getState();
+ },
+});
+
+export default function lifecycle(def = {}) {
+ return component =>
+ component(proto => {
+ const scope = Symbol();
+ const {
+ componentDidInitialize,
+ componentWillMount,
+ componentDidMount,
+ componentWillUpdate,
+ componentDidUpdate,
+ componentWillUnmount,
+ } = proto;
+
+ return {
+ componentDidInitialize() {
+ componentDidInitialize.call(this);
+ this[scope] = createScope(this);
+ if (def.componentDidInitialize) {
+ def.componentDidInitialize.call(this[scope]);
+ }
+ },
+ componentWillMount() {
+ componentWillMount.call(this);
+ if (def.componentWillMount) {
+ def.componentWillMount.call(this[scope]);
+ }
+ },
+ componentDidMount() {
+ componentDidMount.call(this);
+ if (def.componentDidMount) {
+ def.componentDidMount.call(this[scope]);
+ }
+ },
+ componentWillUpdate(newProps, newState) {
+ componentWillUpdate.call(this, newProps, newState);
+ if (def.componentWillUpdate) {
+ def.componentWillUpdate.call(
+ this[scope],
+ newProps,
+ newState,
+ );
+ }
+ },
+ componentDidUpdate(prevProps, prevState) {
+ componentDidUpdate.call(this, prevProps, prevState);
+ if (def.componentDidUpdate) {
+ def.componentDidUpdate.call(
+ this[scope],
+ prevProps,
+ prevState,
+ );
+ }
+ },
+ componentWillUnmount() {
+ componentWillUnmount.call(this);
+ if (def.componentWillUnmount) {
+ def.componentWillUnmount.call(this[scope]);
+ }
+ this[scope] = undefined;
+ },
+ };
+ });
+}
diff --git a/packages/melody-hoc/src/mapProps.js b/packages/melody-hoc/src/mapProps.js
new file mode 100644
index 0000000..fbe0552
--- /dev/null
+++ b/packages/melody-hoc/src/mapProps.js
@@ -0,0 +1,28 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Accepts a function that maps owner props to a new collection of props that are passed to the base component.
+ */
+const mapProps = mapper => Component =>
+ Component(({ apply }) => ({
+ apply(props) {
+ if (props !== this.props) {
+ apply.call(this, mapper(props));
+ }
+ },
+ }));
+
+export default mapProps;
diff --git a/packages/melody-hoc/src/withHandlers.js b/packages/melody-hoc/src/withHandlers.js
new file mode 100644
index 0000000..c6a8126
--- /dev/null
+++ b/packages/melody-hoc/src/withHandlers.js
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2015-2016 Andrew Clark
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const mapValues = (obj, func) => {
+ const result = {};
+ for (const key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ result[key] = func(obj[key], key);
+ }
+ }
+ return result;
+};
+
+const CACHED_HANDLERS = 'MELODY/WITH_HANDLERS/CACHED_HANDLERS';
+const HANDLERS = 'MELODY/WITH_HANDLERS/HANDLERS';
+
+const withHandlers = handlers => Component => {
+ return class WithHandlersComponent extends Component {
+ constructor(...args) {
+ super(...args);
+ this[CACHED_HANDLERS] = {};
+ this[HANDLERS] = mapValues(
+ handlers,
+ (createHandler, handlerName) => (...args) => {
+ const cachedHandler = this[CACHED_HANDLERS][handlerName];
+ if (cachedHandler) {
+ return cachedHandler(...args);
+ }
+
+ const handler = createHandler(this.props);
+ this[CACHED_HANDLERS][handlerName] = handler;
+
+ if (
+ process.env.NODE_ENV !== 'production' &&
+ typeof handler !== 'function'
+ ) {
+ // eslint-disable-next-line no-console
+ console.error(
+ 'withHandlers(): Expected a map of higher-order functions. ' +
+ 'Refer to the docs for more info.',
+ );
+ }
+
+ return handler(...args);
+ },
+ );
+ }
+
+ apply(props) {
+ if (props === this.props) {
+ return;
+ }
+
+ this[CACHED_HANDLERS] = {};
+
+ super.apply({
+ ...props,
+ ...this[HANDLERS],
+ });
+ }
+ };
+};
+
+export default withHandlers;
diff --git a/packages/melody-hoc/src/withProps.js b/packages/melody-hoc/src/withProps.js
new file mode 100644
index 0000000..f3da0ac
--- /dev/null
+++ b/packages/melody-hoc/src/withProps.js
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import mapProps from './mapProps';
+
+/**
+ * Like mapProps(), except the newly created props are merged with the owner props.
+ */
+export default function withProps(additionalProps) {
+ return mapProps(props => Object.assign({}, props, additionalProps));
+}
diff --git a/packages/melody-hoc/src/withRefs.js b/packages/melody-hoc/src/withRefs.js
new file mode 100644
index 0000000..04aa714
--- /dev/null
+++ b/packages/melody-hoc/src/withRefs.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const withRefs = mapper => Component =>
+ Component(({ componentDidInitialize, apply }) => {
+ const refField = Symbol();
+ return {
+ componentDidInitialize() {
+ this[refField] = mapper(this);
+ componentDidInitialize.call(this);
+ },
+ apply(props) {
+ apply.call(this, Object.assign({}, props, this[refField]));
+ },
+ };
+ });
+
+export default withRefs;
diff --git a/packages/melody-hoc/src/withStore.js b/packages/melody-hoc/src/withStore.js
new file mode 100644
index 0000000..05326a7
--- /dev/null
+++ b/packages/melody-hoc/src/withStore.js
@@ -0,0 +1,69 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { RECEIVE_PROPS } from 'melody-component';
+
+const UNSUB = 'MELODY/WITH_STORE/UNSUB';
+const IS_FIRST_APPLY = 'MELODY/WITH_STORE/FIRST_APPLY';
+const STORE = 'MELODY/WITH_STORE/STORE';
+
+const withStore = (
+ storeFactory,
+ stateName = 'state',
+ dispatchName = 'dispatch',
+) => Component => {
+ return class WithStoreComponent extends Component {
+ constructor(...args) {
+ super(...args);
+ this[STORE] = storeFactory();
+ this[IS_FIRST_APPLY] = true;
+ }
+
+ apply(props) {
+ if (props === this.props) {
+ return;
+ }
+
+ this[STORE].dispatch({
+ type: RECEIVE_PROPS,
+ payload: props,
+ });
+
+ if (this[IS_FIRST_APPLY]) {
+ this[UNSUB] = this[STORE].subscribe(() => {
+ super.apply({
+ ...this.props,
+ [stateName]: this[STORE].getState(),
+ [dispatchName]: this[STORE].dispatch,
+ });
+ });
+ this[IS_FIRST_APPLY] = false;
+ }
+
+ super.apply({
+ ...props,
+ [stateName]: this[STORE].getState(),
+ [dispatchName]: this[STORE].dispatch,
+ });
+ }
+
+ componentWillUnmount() {
+ this[UNSUB]();
+ }
+ };
+};
+
+export default withStore;
+export { RECEIVE_PROPS } from 'melody-component';
diff --git a/packages/melody-idom/__tests__/AttributesSpec.ts b/packages/melody-idom/__tests__/AttributesSpec.ts
new file mode 100644
index 0000000..96e53e3
--- /dev/null
+++ b/packages/melody-idom/__tests__/AttributesSpec.ts
@@ -0,0 +1,426 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ patch,
+ elementOpen,
+ elementOpenStart,
+ elementOpenEnd,
+ attr,
+ elementClose,
+ elementVoid,
+} from '../src';
+import { importNode } from '../src/node_data';
+import { expect } from 'chai';
+
+describe('attribute updates', () => {
+ let container;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ describe('for conditional attributes', () => {
+ function render(attrs) {
+ elementOpenStart('div');
+ for (const attrName in attrs) {
+ attr(attrName, attrs[attrName]);
+ }
+ elementOpenEnd();
+ elementClose('div');
+ }
+
+ it('should be present when they have a value', () => {
+ patch(container, () =>
+ render({
+ 'data-expanded': 'hello',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-expanded')).to.equal('hello');
+ });
+
+ it('should be present when falsy', () => {
+ patch(container, () =>
+ render({
+ 'data-expanded': false,
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-expanded')).to.equal('false');
+ });
+
+ it('should be not present when undefined', () => {
+ patch(container, () =>
+ render({
+ id: undefined,
+ tabindex: undefined,
+ 'data-expanded': undefined,
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-expanded')).to.equal(null);
+ expect(el.getAttribute('id')).to.equal(null);
+ expect(el.getAttribute('tabindex')).to.equal(null);
+ });
+
+ it('should update the DOM when they change', () => {
+ patch(container, () =>
+ render({
+ 'data-expanded': 'foo',
+ }),
+ );
+ patch(container, () =>
+ render({
+ 'data-expanded': 'bar',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-expanded')).to.equal('bar');
+ });
+
+ it('should update different attribute in same position', () => {
+ patch(container, () =>
+ render({
+ 'data-foo': 'foo',
+ }),
+ );
+ patch(container, () =>
+ render({
+ 'data-bar': 'foo',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-bar')).to.equal('foo');
+ expect(el.getAttribute('data-foo')).to.equal(null);
+ });
+
+ describe('for attributes in different position', () => {
+ it('should keep attribute that is moved up', () => {
+ patch(container, () =>
+ render({
+ 'data-foo': 'foo',
+ 'data-bar': 'bar',
+ }),
+ );
+ patch(container, () =>
+ render({
+ 'data-bar': 'bar',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-foo')).to.equal(null);
+ expect(el.getAttribute('data-bar')).to.equal('bar');
+ });
+
+ it('should keep attribute that is moved back', () => {
+ patch(container, () =>
+ render({
+ 'data-bar': 'bar',
+ }),
+ );
+ patch(container, () =>
+ render({
+ 'data-foo': 'foo',
+ 'data-bar': 'bar',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-foo')).to.equal('foo');
+ expect(el.getAttribute('data-bar')).to.equal('bar');
+ });
+
+ it('should keep attribute that is moved up then kept', () => {
+ patch(container, () =>
+ render({
+ 'data-foo': 'foo',
+ 'data-bar': 'bar',
+ 'data-baz': 'baz',
+ }),
+ );
+ patch(container, () =>
+ render({
+ 'data-bar': 'bar',
+ 'data-baz': 'baz',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-foo')).to.equal(null);
+ expect(el.getAttribute('data-bar')).to.equal('bar');
+ expect(el.getAttribute('data-baz')).to.equal('baz');
+
+ patch(container, () =>
+ render({
+ 'data-bar': 'bar',
+ }),
+ );
+ expect(el.getAttribute('data-foo')).to.equal(null);
+ expect(el.getAttribute('data-bar')).to.equal('bar');
+ expect(el.getAttribute('data-baz')).to.equal(null);
+ });
+
+ it('should keep attribute that is backwards up then kept', () => {
+ patch(container, () =>
+ render({
+ 'data-bar': 'bar',
+ 'data-baz': 'baz',
+ }),
+ );
+ patch(container, () =>
+ render({
+ 'data-foo': 'foo',
+ 'data-bar': 'bar',
+ 'data-baz': 'baz',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-foo')).to.equal('foo');
+ expect(el.getAttribute('data-bar')).to.equal('bar');
+ expect(el.getAttribute('data-baz')).to.equal('baz');
+
+ patch(container, () =>
+ render({
+ 'data-foo': 'foo',
+ 'data-bar': 'bar',
+ }),
+ );
+ expect(el.getAttribute('data-foo')).to.equal('foo');
+ expect(el.getAttribute('data-bar')).to.equal('bar');
+ expect(el.getAttribute('data-baz')).to.equal(null);
+ });
+ });
+
+ it('should remove trailing attributes when missing', function() {
+ patch(container, () =>
+ render({
+ 'data-foo': 'foo',
+ 'data-bar': 'bar',
+ }),
+ );
+ patch(container, () => render({}));
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-foo')).to.equal(null);
+ expect(el.getAttribute('data-bar')).to.equal(null);
+ });
+ });
+
+ describe('for function attributes', () => {
+ it('should not be set as attributes', () => {
+ const fn = () => {};
+ patch(container, () => {
+ elementVoid('div', null, null, 'fn', fn);
+ });
+ const el = container.childNodes[0];
+
+ expect(el.hasAttribute('fn')).to.be.false;
+ });
+
+ it('should be set on the node', () => {
+ const fn = () => {};
+ patch(container, () => {
+ elementVoid('div', null, null, 'fn', fn);
+ });
+ const el = container.childNodes[0];
+
+ expect(el.fn).to.equal(fn);
+ });
+ });
+
+ describe('for object attributes', () => {
+ it('should not be set as attributes', () => {
+ const obj = {};
+ patch(container, () => {
+ elementVoid('div', null, null, 'obj', obj);
+ });
+ const el = container.childNodes[0];
+
+ expect(el.hasAttribute('obj')).to.be.false;
+ });
+
+ it('should be set on the node', () => {
+ const obj = {};
+ patch(container, () => {
+ elementVoid('div', null, null, 'obj', obj);
+ });
+ const el = container.childNodes[0];
+
+ expect(el.obj).to.equal(obj);
+ });
+ });
+
+ describe('for svg elements', () => {
+ it('should correctly apply the class attribute', function() {
+ patch(container, () => {
+ elementVoid('svg', null, null, 'class', 'foo');
+ });
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('class')).to.equal('foo');
+ });
+
+ it('should apply the correct namespace for namespaced SVG attributes', () => {
+ patch(container, () => {
+ elementOpen('svg');
+ elementVoid('image', null, null, 'xlink:href', '#foo');
+ elementClose('svg');
+ });
+ const el = container.childNodes[0].childNodes[0];
+ expect(
+ el.getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
+ ).to.equal('#foo');
+ });
+
+ it('should remove namespaced SVG attributes', () => {
+ patch(container, () => {
+ elementOpen('svg');
+ elementVoid('image', null, null, 'xlink:href', '#foo');
+ elementClose('svg');
+ });
+ patch(container, () => {
+ elementOpen('svg');
+ elementVoid('image', null, null);
+ elementClose('svg');
+ });
+ const el = container.childNodes[0].childNodes[0];
+ expect(el.hasAttributeNS('http://www.w3.org/1999/xlink', 'href')).to
+ .be.false;
+ });
+ });
+
+ describe('for non-Incremental DOM attributes', () => {
+ function render() {
+ elementVoid('div');
+ }
+
+ it('should be preserved when changed between patches', () => {
+ patch(container, render);
+ const el = container.firstChild;
+ el.setAttribute('data-foo', 'bar');
+ patch(container, render);
+
+ expect(el.getAttribute('data-foo')).to.equal('bar');
+ });
+
+ it('should be preserved when importing DOM', () => {
+ container.innerHTML = '
';
+
+ importNode(container);
+ const el = container.firstChild;
+ el.setAttribute('data-foo', 'bar');
+ patch(container, render);
+
+ expect(el.getAttribute('data-foo')).to.equal('bar');
+ });
+ });
+
+ describe('with an existing document tree', () => {
+ let div;
+
+ beforeEach(function() {
+ div = document.createElement('div');
+ div.setAttribute('class', 'foo');
+ div.setAttribute('tabindex', '-1');
+ container.appendChild(div);
+ });
+
+ it('should update attributes', () => {
+ function render() {
+ elementVoid('div', null, ['class', 'bar'], 'tabindex', '0');
+ }
+
+ patch(container, render);
+ let child = container.childNodes[0];
+
+ expect(child.getAttribute('tabindex')).to.equal('0');
+ expect(child.getAttribute('class')).to.equal('bar');
+
+ patch(container, render);
+ child = container.childNodes[0];
+
+ expect(child.getAttribute('tabindex')).to.equal('0');
+ expect(child.getAttribute('class')).to.equal('bar');
+ });
+
+ it('should remove attributes', () => {
+ function render(data) {
+ elementVoid('div', null, ['class', 'foo'], data.attr, 'bar');
+ }
+ patch(container, render, { attr: 'data-foo' });
+ const child = container.childNodes[0];
+
+ expect(child.hasAttribute('tabindex')).to.false;
+ expect(child.hasAttribute('class')).to.true;
+ expect(child.getAttribute('data-foo')).to.equal('bar');
+
+ patch(container, render, { attr: 'data-bar' });
+ expect(child.hasAttribute('tabindex')).to.false;
+ expect(child.hasAttribute('data-foo')).to.false;
+ expect(child.getAttribute('data-bar')).to.equal('bar');
+ });
+
+ it('should persist statics', () => {
+ function render(value) {
+ elementVoid('div', null, ['data-foo', value]);
+ }
+
+ patch(container, render, 'bar');
+ const child = container.childNodes[0];
+
+ expect(child.getAttribute('data-foo')).to.equal('bar');
+
+ patch(container, render, 'baz');
+ expect(child.getAttribute('data-foo')).to.equal('bar');
+ });
+
+ it('should persist statics when patching a parent', () => {
+ function renderParent(value) {
+ elementOpen('div');
+ render(value);
+ elementClose('div');
+ }
+ function render(value) {
+ elementVoid('div', null, ['data-foo', value]);
+ }
+
+ const child = container.childNodes[0];
+ patch(child, render, 'bar');
+ const grandChild = child.childNodes[0];
+
+ expect(grandChild.getAttribute('data-foo')).to.equal('bar');
+
+ patch(container, renderParent, 'baz');
+
+ expect(child.hasAttribute('tabindex')).to.false;
+ expect(grandChild.getAttribute('data-foo')).to.equal('bar');
+ });
+ });
+});
diff --git a/packages/melody-idom/__tests__/ComponentSpec.ts b/packages/melody-idom/__tests__/ComponentSpec.ts
new file mode 100644
index 0000000..0343c7f
--- /dev/null
+++ b/packages/melody-idom/__tests__/ComponentSpec.ts
@@ -0,0 +1,266 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ patch,
+ elementOpen,
+ text,
+ component,
+ elementClose,
+ enqueueComponent,
+ flush,
+ mount,
+ link,
+ getParent,
+} from '../src';
+import { getChildren } from '../src/hierarchy';
+
+describe('component', function() {
+ let unmounted = false;
+ let notified = false;
+ let el = null;
+
+ beforeEach(function() {
+ unmounted = false;
+ notified = false;
+ el = document.createElement('div');
+ });
+
+ class Component {
+ constructor() {
+ this.el = null;
+ this.refs = Object.create(null);
+ this.props = null;
+ }
+
+ apply(props) {
+ this.props = props;
+ enqueueComponent(this);
+ }
+
+ notify() {
+ notified = true;
+ }
+
+ componentWillUnmount() {
+ unmounted = true;
+ }
+
+ render() {
+ elementOpen('div');
+ text(this.props.text);
+ elementClose('div');
+ }
+ }
+
+ class ParentComponent extends Component {
+ render() {
+ elementOpen('section');
+ component(Component, 'child1', this.props.firstChild);
+ component(Component, 'child2', this.props.secondChild);
+ elementClose('section');
+ }
+ }
+
+ it('should render the component', function() {
+ patch(
+ el,
+ data => {
+ component(Component, 'test', data);
+ },
+ { text: 'Hello' },
+ );
+ expect(el.innerHTML).toEqual(' ');
+ run();
+ expect(el.innerHTML).toEqual('Hello
');
+ expect(unmounted).toEqual(false);
+ });
+
+ it('should mount the component', function() {
+ patch(
+ el,
+ data => {
+ mount(el, Component, data);
+ },
+ { text: 'Hello' },
+ );
+ run();
+ expect(el.outerHTML).toEqual('Hello
');
+ });
+
+ it('should notify the component', function() {
+ patch(
+ el,
+ data => {
+ component(Component, 'test', data);
+ },
+ { text: 'Hello' },
+ );
+ run();
+ expect(notified).toEqual(true);
+ });
+
+ it('should invoke componentWillUnmount when the component is removed', function() {
+ patch(
+ el,
+ data => {
+ component(Component, 'test', data);
+ },
+ { text: 'Hello' },
+ );
+ run();
+ patch(
+ el,
+ data => {
+ elementOpen('div');
+ elementClose('div');
+ },
+ { text: 'Hello' },
+ );
+ expect(unmounted).toEqual(true);
+ });
+
+ it('should render in multiple stages', function() {
+ // initial rendering happens immediately
+ patch(
+ el,
+ data => {
+ component(ParentComponent, 'test', {
+ firstChild: { text: 'Hello' },
+ secondChild: { text: 'World' },
+ });
+ },
+ {},
+ );
+ expect(el.innerHTML).toEqual(' ');
+ run(1);
+ expect(el.innerHTML).toEqual(
+ '',
+ );
+
+ patch(
+ el,
+ data => {
+ component(ParentComponent, 'test', {
+ firstChild: { text: 'hello' },
+ secondChild: { text: 'universe' },
+ });
+ },
+ {},
+ );
+ run(1);
+ // Updates outer element
+ expect(el.innerHTML).toEqual(
+ '',
+ );
+ run(1);
+ // updates first child component
+ expect(el.innerHTML).toEqual(
+ '',
+ );
+ run(1);
+ // updates second child component
+ expect(el.innerHTML).toEqual(
+ '',
+ );
+ });
+
+ it('should render new hierachies immediately', function() {
+ patch(
+ el,
+ data => {
+ component(ParentComponent, 'test', {
+ firstChild: { text: 'Hello' },
+ secondChild: { text: 'World' },
+ });
+ },
+ {},
+ );
+ run(1);
+ expect(el.innerHTML).toEqual(
+ '',
+ );
+ });
+
+ it('should mount existing DOM in multiple stages', function() {
+ el.innerHTML =
+ '';
+
+ patch(
+ el,
+ data => {
+ component(ParentComponent, 'test', {
+ firstChild: { text: 'hello' },
+ secondChild: { text: 'universe' },
+ });
+ },
+ {},
+ );
+ run(1);
+ // Updates outer element
+ expect(el.innerHTML).toEqual(
+ '',
+ );
+ run(1);
+ // updates first child component
+ expect(el.innerHTML).toEqual(
+ '',
+ );
+ run(1);
+ // updates second child component
+ expect(el.innerHTML).toEqual(
+ '',
+ );
+ });
+
+ it('should continue rendering', function() {
+ jest.useFakeTimers();
+ patch(
+ el,
+ data => {
+ component(ParentComponent, 'test', {
+ firstChild: { text: 'Hello' },
+ secondChild: { text: 'World' },
+ });
+ },
+ {},
+ );
+ expect(el.innerHTML).toEqual(' ');
+ run(1);
+ jest.runAllTimers();
+ expect(el.innerHTML).toEqual(
+ '',
+ );
+ });
+
+ it('should link parent and children', function() {
+ const a = new Component();
+ const b = new Component();
+ link(a, b);
+ expect(getParent(b)).toBe(a);
+ expect(getChildren(a)).toEqual([b]);
+ });
+});
+
+function run(rounds = 1) {
+ for (var i = 0; i < rounds; i++) {
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 0;
+ },
+ });
+ }
+}
diff --git a/packages/melody-idom/__tests__/ConditionalRenderingSpec.ts b/packages/melody-idom/__tests__/ConditionalRenderingSpec.ts
new file mode 100644
index 0000000..f16aeb6
--- /dev/null
+++ b/packages/melody-idom/__tests__/ConditionalRenderingSpec.ts
@@ -0,0 +1,153 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { patch, elementOpen, elementClose, elementVoid } from '../src';
+import { expect } from 'chai';
+
+describe('conditional rendering', () => {
+ let container;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ describe('nodes', () => {
+ function render(condition) {
+ elementOpen('div', 'outer', ['id', 'outer']);
+ elementVoid('div', 'one', ['id', 'one']);
+
+ if (condition) {
+ elementVoid('div', 'conditional-one', [
+ 'id',
+ 'conditional-one',
+ ]);
+ elementVoid('div', 'conditional-two', [
+ 'id',
+ 'conditional-two',
+ ]);
+ }
+
+ elementVoid('span', 'two', ['id', 'two']);
+ elementClose('div');
+ }
+
+ it('should un-render when the condition becomes false', () => {
+ patch(container, () => render(true));
+ patch(container, () => render(false));
+ const outer = container.childNodes[0];
+
+ expect(outer.childNodes).to.have.length(2);
+ expect(outer.childNodes[0].id).to.equal('one');
+ expect(outer.childNodes[0].tagName).to.equal('DIV');
+ expect(outer.childNodes[1].id).to.equal('two');
+ expect(outer.childNodes[1].tagName).to.equal('SPAN');
+ });
+
+ it('should render when the condition becomes true', () => {
+ patch(container, () => render(false));
+ patch(container, () => render(true));
+ const outer = container.childNodes[0];
+
+ expect(outer.childNodes).to.have.length(4);
+ expect(outer.childNodes[0].id).to.equal('one');
+ expect(outer.childNodes[0].tagName).to.equal('DIV');
+ expect(outer.childNodes[1].id).to.equal('conditional-one');
+ expect(outer.childNodes[1].tagName).to.equal('DIV');
+ expect(outer.childNodes[2].id).to.equal('conditional-two');
+ expect(outer.childNodes[2].tagName).to.equal('DIV');
+ expect(outer.childNodes[3].id).to.equal('two');
+ expect(outer.childNodes[3].tagName).to.equal('SPAN');
+ });
+ });
+
+ describe('with only conditional childNodes', () => {
+ function render(condition) {
+ elementOpen('div', 'outer', ['id', 'outer']);
+
+ if (condition) {
+ elementVoid('div', 'conditional-one', [
+ 'id',
+ 'conditional-one',
+ ]);
+ elementVoid('div', 'conditional-two', [
+ 'id',
+ 'conditional-two',
+ ]);
+ }
+
+ elementClose('div');
+ }
+
+ it('should not leave any remaning nodes', () => {
+ patch(container, () => render(true));
+ patch(container, () => render(false));
+ const outer = container.childNodes[0];
+
+ expect(outer.childNodes).to.have.length(0);
+ });
+ });
+
+ describe('nodes', () => {
+ function render(condition) {
+ elementOpen('div', null, null, 'id', 'outer');
+ elementVoid('div', null, null, 'id', 'one');
+
+ if (condition) {
+ elementOpen(
+ 'span',
+ null,
+ null,
+ 'id',
+ 'conditional-one',
+ 'data-foo',
+ 'foo',
+ );
+ elementVoid('span');
+ elementClose('span');
+ }
+
+ elementVoid('span', null, null, 'id', 'two');
+ elementClose('div');
+ }
+
+ it('should strip children when a conflicting node is re-used', () => {
+ patch(container, () => render(true));
+ patch(container, () => render(false));
+ const outer = container.childNodes[0];
+
+ expect(outer.childNodes).to.have.length(2);
+ expect(outer.childNodes[0].id).to.equal('one');
+ expect(outer.childNodes[0].tagName).to.equal('DIV');
+ expect(outer.childNodes[1].id).to.equal('two');
+ expect(outer.childNodes[1].tagName).to.equal('SPAN');
+ expect(outer.childNodes[1].children.length).to.equal(0);
+ });
+
+ it('should strip attributes when a conflicting node is re-used', () => {
+ patch(container, () => render(true));
+ patch(container, () => render(false));
+ const outer = container.childNodes[0];
+
+ expect(outer.childNodes[1].getAttribute('data-foo')).to.be.null;
+ });
+ });
+});
diff --git a/packages/melody-idom/__tests__/CurrentElementSpec.ts b/packages/melody-idom/__tests__/CurrentElementSpec.ts
new file mode 100644
index 0000000..8c56a59
--- /dev/null
+++ b/packages/melody-idom/__tests__/CurrentElementSpec.ts
@@ -0,0 +1,100 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ patch,
+ elementOpen,
+ elementOpenStart,
+ elementOpenEnd,
+ elementClose,
+ elementVoid,
+ currentElement,
+} from '../src';
+import { expect } from 'chai';
+
+describe('currentElement', () => {
+ let container;
+ let el;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ el = null;
+ });
+
+ it('should return the element from elementOpen', () => {
+ patch(container, () => {
+ elementOpen('div');
+ el = currentElement();
+ elementClose('div');
+ });
+ expect(el).to.equal(container.childNodes[0]);
+ });
+
+ it('should return the element from elementOpenEnd', () => {
+ patch(container, () => {
+ elementOpenStart('div');
+ elementOpenEnd('div');
+ el = currentElement();
+ elementClose('div');
+ });
+
+ expect(el).to.equal(container.childNodes[0]);
+ });
+
+ it('should return the parent after elementClose', () => {
+ patch(container, () => {
+ elementOpen('div');
+ elementClose('div');
+ el = currentElement();
+ });
+
+ expect(el).to.equal(container);
+ });
+
+ it('should return the parent after elementVoid', () => {
+ patch(container, () => {
+ elementVoid('div');
+ el = currentElement();
+ });
+
+ expect(el).to.equal(container);
+ });
+
+ it('should throw an error if not patching', () => {
+ expect(currentElement).to.throw(
+ 'Cannot call currentElement() unless in patch',
+ );
+ });
+
+ it('should throw an error if inside virtual attributes element', () => {
+ expect(() => {
+ patch(container, () => {
+ elementOpenStart('div');
+ el = currentElement();
+ elementOpenEnd('div');
+ elementClose('div');
+ });
+ }).to.throw(
+ 'currentElement() can not be called between elementOpenStart() and elementOpenEnd().',
+ );
+ });
+});
diff --git a/packages/melody-idom/__tests__/ElementCreationSpec.ts b/packages/melody-idom/__tests__/ElementCreationSpec.ts
new file mode 100644
index 0000000..983f4df
--- /dev/null
+++ b/packages/melody-idom/__tests__/ElementCreationSpec.ts
@@ -0,0 +1,214 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ patch,
+ elementOpen,
+ elementOpenStart,
+ elementOpenEnd,
+ elementClose,
+ elementVoid,
+} from '../src';
+import sinon from 'sinon';
+import { expect } from 'chai';
+
+describe('element creation', () => {
+ let container;
+ let sandbox = sinon.sandbox.create();
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ document.body.removeChild(container);
+ });
+
+ describe('when creating a single node', () => {
+ let el;
+
+ beforeEach(() => {
+ patch(container, () => {
+ elementVoid(
+ 'div',
+ 'key',
+ [
+ 'id',
+ 'someId',
+ 'class',
+ 'someClass',
+ 'data-custom',
+ 'custom',
+ ],
+ 'data-foo',
+ 'Hello',
+ 'data-bar',
+ 'World',
+ );
+ });
+
+ el = container.childNodes[0];
+ });
+
+ it('should render with the specified tag', () => {
+ expect(el.tagName).to.equal('DIV');
+ });
+
+ it('should render with static attributes', () => {
+ expect(el.id).to.equal('someId');
+ expect(el.className).to.equal('someClass');
+ expect(el.getAttribute('data-custom')).to.equal('custom');
+ });
+
+ it('should render with dynamic attributes', () => {
+ expect(el.getAttribute('data-foo')).to.equal('Hello');
+ expect(el.getAttribute('data-bar')).to.equal('World');
+ });
+
+ describe('should return DOM node', () => {
+ beforeEach(() => {
+ patch(container, () => {});
+ });
+
+ it('from elementOpen', () => {
+ patch(container, () => {
+ el = elementOpen('div');
+ elementClose('div');
+ });
+
+ expect(el).to.equal(container.childNodes[0]);
+ });
+
+ it('from elementClose', () => {
+ patch(container, () => {
+ elementOpen('div');
+ el = elementClose('div');
+ });
+
+ expect(el).to.equal(container.childNodes[0]);
+ });
+
+ it('from elementVoid', () => {
+ patch(container, () => {
+ el = elementVoid('div');
+ });
+
+ expect(el).to.equal(container.childNodes[0]);
+ });
+
+ it('from elementOpenEnd', () => {
+ patch(container, () => {
+ elementOpenStart('div');
+ el = elementOpenEnd('div');
+ elementClose('div');
+ });
+
+ expect(el).to.equal(container.childNodes[0]);
+ });
+ });
+ });
+
+ it('should allow creation without static attributes', () => {
+ patch(container, () => {
+ elementVoid('div', null, null, 'id', 'test');
+ });
+ const el = container.childNodes[0];
+ expect(el.id).to.equal('test');
+ });
+
+ describe('for HTML elements', () => {
+ it('should use the XHTML namespace', () => {
+ patch(container, () => {
+ elementVoid('div');
+ });
+
+ const el = container.childNodes[0];
+ expect(el.namespaceURI).to.equal('http://www.w3.org/1999/xhtml');
+ });
+
+ it('should use createElement if no namespace has been specified', () => {
+ let doc = container.ownerDocument;
+ let div = doc.createElement('div');
+ let el;
+ sandbox.stub(doc, 'createElement').returns(div);
+
+ patch(container, () => {
+ elementOpen('svg');
+ elementOpen('foreignObject');
+ el = elementVoid('div');
+ elementClose('foreignObject');
+ elementClose('svg');
+ });
+
+ expect(el.namespaceURI).to.equal('http://www.w3.org/1999/xhtml');
+ expect(doc.createElement).to.have.been.calledOnce;
+ });
+ });
+
+ describe('for svg elements', () => {
+ beforeEach(() => {
+ patch(container, () => {
+ elementOpen('svg');
+ elementOpen('g');
+ elementVoid('circle');
+ elementClose('g');
+ elementOpen('foreignObject');
+ elementVoid('p');
+ elementClose('foreignObject');
+ elementVoid('path');
+ elementClose('svg');
+ });
+ });
+
+ it('should create svgs in the svg namespace', () => {
+ const el = container.querySelector('svg');
+ expect(el.namespaceURI).to.equal('http://www.w3.org/2000/svg');
+ });
+
+ it('should create descendants of svgs in the svg namespace', () => {
+ const el = container.querySelector('circle');
+ expect(el.namespaceURI).to.equal('http://www.w3.org/2000/svg');
+ });
+
+ it('should have the svg namespace for foreignObjects', () => {
+ const el = container.querySelector('svg').childNodes[1];
+ expect(el.namespaceURI).to.equal('http://www.w3.org/2000/svg');
+ });
+
+ it('should revert to the xhtml namespace when encounering a foreignObject', () => {
+ const el = container.querySelector('p');
+ expect(el.namespaceURI).to.equal('http://www.w3.org/1999/xhtml');
+ });
+
+ it('should reset to the previous namespace after exiting a forignObject', () => {
+ const el = container.querySelector('path');
+ expect(el.namespaceURI).to.equal('http://www.w3.org/2000/svg');
+ });
+
+ it('should create children in the svg namespace when patching an svg', () => {
+ const svg = container.querySelector('svg');
+ patch(svg, () => {
+ elementVoid('rect');
+ });
+
+ const el = svg.querySelector('rect');
+ expect(el.namespaceURI).to.equal('http://www.w3.org/2000/svg');
+ });
+ });
+});
diff --git a/packages/melody-idom/__tests__/FormattersSpec.ts b/packages/melody-idom/__tests__/FormattersSpec.ts
new file mode 100644
index 0000000..4b169a2
--- /dev/null
+++ b/packages/melody-idom/__tests__/FormattersSpec.ts
@@ -0,0 +1,94 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { patch, text } from '../src';
+import sinon from 'sinon';
+import chai from 'chai';
+import sinonChai from 'sinon-chai';
+chai.use(sinonChai);
+import { expect } from 'chai';
+
+describe('formatters', () => {
+ let container;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ describe('for newly created Text nodes', () => {
+ function sliceOne(str) {
+ return str.slice(1);
+ }
+
+ function prefixQuote(str) {
+ return "'" + str;
+ }
+
+ it('should render with the specified formatted value', () => {
+ patch(container, () => {
+ text('hello world!', sliceOne, prefixQuote);
+ });
+ const node = container.childNodes[0];
+
+ expect(node.textContent).to.equal("'ello world!");
+ });
+ });
+
+ describe('for updated Text nodes', () => {
+ let stub;
+
+ function render(value) {
+ text(value, stub);
+ }
+
+ beforeEach(() => {
+ stub = sinon.stub();
+ stub.onFirstCall().returns('stubValueOne');
+ stub.onSecondCall().returns('stubValueTwo');
+ });
+
+ it('should not call the formatter for unchanged values', () => {
+ patch(container, () => render('hello'));
+ patch(container, () => render('hello'));
+ const node = container.childNodes[0];
+
+ expect(node.textContent).to.equal('stubValueOne');
+ expect(stub).to.have.been.calledOnce;
+ });
+
+ it('should call the formatter when the value changes', () => {
+ patch(container, () => render('hello'));
+ patch(container, () => render('world'));
+ const node = container.childNodes[0];
+
+ expect(node.textContent).to.equal('stubValueTwo');
+ expect(stub).to.have.been.calledTwice;
+ });
+ });
+
+ it('should not leak the arguments object', () => {
+ const stub = sinon.stub().returns('value');
+ patch(container, () => text('value', stub));
+
+ expect(stub).to.have.been.calledOn(undefined);
+ });
+});
diff --git a/packages/melody-idom/__tests__/ImportElementSpec.ts b/packages/melody-idom/__tests__/ImportElementSpec.ts
new file mode 100644
index 0000000..0d4279a
--- /dev/null
+++ b/packages/melody-idom/__tests__/ImportElementSpec.ts
@@ -0,0 +1,84 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { patch, elementVoid, elementOpen, elementClose } from '../src';
+import { importNode } from '../built/node_data';
+import sinon from 'sinon';
+import { expect } from 'chai';
+
+describe('importing element', () => {
+ let container;
+ let sandbox = sinon.sandbox.create();
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ document.body.removeChild(container);
+ });
+
+ describe('in HTML', () => {
+ it('handles normal nodeName capitalization', () => {
+ container.innerHTML = '
';
+ importNode(container);
+
+ const el = container.firstChild;
+ patch(container, () => elementVoid('div'));
+ expect(container.firstChild).to.equal(el);
+ });
+
+ it('handles odd nodeName capitalization', () => {
+ container.innerHTML = '
';
+ importNode(container);
+
+ const el = container.firstChild;
+ patch(container, () => elementVoid('div'));
+ expect(container.firstChild).to.equal(el);
+ });
+ });
+
+ describe('in SVG', () => {
+ it('handles normal nodeName capitalization', () => {
+ container.innerHTML = ' ';
+ importNode(container);
+
+ const foreign = container.firstChild.firstChild;
+ patch(container, () => {
+ elementOpen('svg');
+ elementVoid('foreignObject');
+ elementClose('svg');
+ });
+ expect(container.firstChild.firstChild).to.equal(foreign);
+ });
+
+ it('handles odd nodeName capitalization', () => {
+ container.innerHTML = ' ';
+ importNode(container);
+
+ const foreign = container.firstChild.firstChild;
+ patch(container, () => {
+ elementOpen('svg');
+ elementVoid('foreignObject');
+ elementClose('svg');
+ });
+ expect(container.firstChild.firstChild).to.equal(foreign);
+ });
+ });
+});
diff --git a/packages/melody-idom/__tests__/KeyedItemsSpec.ts b/packages/melody-idom/__tests__/KeyedItemsSpec.ts
new file mode 100644
index 0000000..ffafb2c
--- /dev/null
+++ b/packages/melody-idom/__tests__/KeyedItemsSpec.ts
@@ -0,0 +1,299 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ patch,
+ elementOpen,
+ elementClose,
+ elementVoid,
+ currentElement,
+ skip,
+} from '../src';
+import { expect } from 'chai';
+
+const BROWSER_SUPPORTS_SHADOW_DOM = 'ShadowRoot' in window;
+const attachShadow = function(el) {
+ return el.attachShadow
+ ? el.attachShadow({ mode: 'closed' })
+ : el.createShadowRoot();
+};
+
+describe('rendering with keys', () => {
+ let container;
+
+ function render(items) {
+ for (let i = 0; i < items.length; i++) {
+ const key = items[i].key;
+ elementVoid('div', key, key ? ['id', key] : null);
+ }
+ }
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ it('should not re-use a node with a non-null key', () => {
+ const items = [{ key: 'one' }];
+
+ patch(container, () => render(items));
+ const keyedNode = container.childNodes[0];
+
+ items.unshift({ key: null });
+ patch(container, () => render(items));
+
+ expect(container.childNodes).to.have.length(2);
+ expect(container.childNodes[0]).to.not.equal(keyedNode);
+ });
+
+ it('should not modify DOM nodes with falsey keys', () => {
+ const slice = Array.prototype.slice;
+ const items = [{ key: null }, { key: undefined }, { key: '' }];
+
+ patch(container, () => render(items));
+ const nodes = slice.call(container.childNodes);
+
+ patch(container, () => render(items));
+
+ expect(slice.call(container.childNodes)).to.deep.equal(nodes);
+ });
+
+ it('should not modify the DOM nodes when inserting', () => {
+ const items = [{ key: 'one' }, { key: 'two' }];
+
+ patch(container, () => render(items));
+ const firstNode = container.childNodes[0];
+ const secondNode = container.childNodes[1];
+
+ items.splice(1, 0, { key: 'one-point-five' });
+ patch(container, () => render(items));
+
+ expect(container.childNodes).to.have.length(3);
+ expect(container.childNodes[0]).to.equal(firstNode);
+ expect(container.childNodes[0].id).to.equal('one');
+ expect(container.childNodes[1].id).to.equal('one-point-five');
+ expect(container.childNodes[2]).to.equal(secondNode);
+ expect(container.childNodes[2].id).to.equal('two');
+ });
+
+ it('should not modify the DOM nodes when removing', () => {
+ const items = [{ key: 'one' }, { key: 'two' }, { key: 'three' }];
+
+ patch(container, () => render(items));
+ const firstNode = container.childNodes[0];
+ const thirdNode = container.childNodes[2];
+
+ items.splice(1, 1);
+ patch(container, () => render(items));
+
+ expect(container.childNodes).to.have.length(2);
+ expect(container.childNodes[0]).to.equal(firstNode);
+ expect(container.childNodes[0].id).to.equal('one');
+ expect(container.childNodes[1]).to.equal(thirdNode);
+ expect(container.childNodes[1].id).to.equal('three');
+ });
+
+ it('should not modify the DOM nodes when re-ordering', () => {
+ const items = [{ key: 'one' }, { key: 'two' }, { key: 'three' }];
+
+ patch(container, () => render(items));
+ const firstNode = container.childNodes[0];
+ const secondNode = container.childNodes[1];
+ const thirdNode = container.childNodes[2];
+
+ items.splice(1, 1);
+ items.push({ key: 'two' });
+ patch(container, () => render(items));
+
+ expect(container.childNodes).to.have.length(3);
+ expect(container.childNodes[0]).to.equal(firstNode);
+ expect(container.childNodes[0].id).to.equal('one');
+ expect(container.childNodes[1]).to.equal(thirdNode);
+ expect(container.childNodes[1].id).to.equal('three');
+ expect(container.childNodes[2]).to.equal(secondNode);
+ expect(container.childNodes[2].id).to.equal('two');
+ });
+
+ it('should avoid collisions with Object.prototype', () => {
+ const items = [{ key: 'hasOwnProperty' }];
+
+ patch(container, () => render(items));
+ expect(container.childNodes).to.have.length(1);
+ });
+
+ it("should not reuse dom node when nodeName doesn't match", () => {
+ function render(tag) {
+ elementVoid(tag, 'key');
+ }
+
+ patch(container, render, 'div');
+ const firstNode = container.childNodes[0];
+
+ patch(container, render, 'span');
+ const newNode = container.childNodes[0];
+ expect(newNode).not.to.equal(firstNode);
+ expect(newNode.nodeName).to.equal('SPAN');
+ expect(firstNode.parentNode).to.equal(null);
+ });
+
+ it('should preserve nodes already in the DOM', () => {
+ function render() {
+ elementVoid('div', 'key');
+ elementVoid('div');
+ }
+
+ container.innerHTML = `
+
+
+ `;
+ const keyedDiv = container.lastChild;
+
+ patch(container, render);
+
+ expect(container.firstChild).to.equal(keyedDiv);
+ });
+
+ it('should remove keyed nodes whose element type changes', () => {
+ function render() {
+ elementVoid('div', 'key');
+ elementVoid('div');
+ }
+
+ container.innerHTML = `
+
+
+ `;
+
+ patch(container, render);
+
+ expect(container.innerHTML).to.equal('
');
+ });
+
+ describe('an item with focus', () => {
+ function render(items) {
+ for (let i = 0; i < items.length; i++) {
+ const key = items[i].key;
+ elementOpen('div', key);
+ elementVoid('div', null, null, 'id', key, 'tabindex', -1);
+ elementClose('div');
+ }
+ }
+
+ it('should retain focus when prepending a new item', () => {
+ const items = [{ key: 'one' }];
+
+ patch(container, () => render(items));
+ const focusNode = container.querySelector('#one');
+ focusNode.focus();
+
+ items.unshift({ key: 'zero' });
+ patch(container, () => render(items));
+
+ expect(document.activeElement).to.equal(focusNode);
+ });
+
+ it('should retain focus when moving up in DOM order', () => {
+ const items = [{ key: 'one' }, { key: 'two' }, { key: 'three' }];
+
+ patch(container, () => render(items));
+ const focusNode = container.querySelector('#three');
+ focusNode.focus();
+
+ items.unshift(items.pop());
+ patch(container, () => render(items));
+
+ expect(document.activeElement).to.equal(focusNode);
+ });
+
+ it('should retain focus when moving down in DOM order', () => {
+ const items = [{ key: 'one' }, { key: 'two' }, { key: 'three' }];
+
+ patch(container, () => render(items));
+ const focusNode = container.querySelector('#one');
+ focusNode.focus();
+
+ items.push(items.shift());
+ patch(container, () => render(items));
+
+ expect(document.activeElement).to.equal(focusNode);
+ });
+
+ it('should retain focus when doing a nested patch', () => {
+ function renderInner(id) {
+ elementVoid('div', null, null, 'id', id, 'tabindex', -1);
+ }
+
+ function render() {
+ for (let i = 0; i < items.length; i++) {
+ const key = items[i].key;
+ elementOpen('div', key, null);
+ patch(currentElement(), () => renderInner(key));
+ skip();
+ elementClose('div');
+ }
+ }
+
+ const items = [{ key: 'one' }, { key: 'two' }, { key: 'three' }];
+
+ patch(container, () => render(items));
+ const focusNode = container.querySelector('#three');
+ focusNode.focus();
+
+ items.unshift(items.pop());
+ patch(container, () => render(items));
+
+ expect(document.activeElement).to.equal(focusNode);
+ });
+
+ if (BROWSER_SUPPORTS_SHADOW_DOM) {
+ it('should retain focus when patching a ShadowRoot', () => {
+ const items = [{ key: 'one' }];
+
+ const shadowRoot = attachShadow(container);
+ patch(shadowRoot, () => render(items));
+ const focusNode = shadowRoot.querySelector('#one');
+ focusNode.focus();
+
+ items.unshift({ key: 'zero' });
+ patch(shadowRoot, () => render(items));
+
+ expect(shadowRoot.activeElement).to.equal(focusNode);
+ expect(document.activeElement).to.equal(container);
+ });
+
+ it('should retain focus when patching outside a ShadowRoot', () => {
+ const items = [{ key: 'one' }];
+
+ const shadowRoot = attachShadow(container);
+ const shadowEl = shadowRoot.appendChild(
+ document.createElement('div'),
+ );
+ shadowEl.tabIndex = -1;
+ shadowEl.focus();
+
+ items.unshift({ key: 'zero' });
+ patch(container, () => render(items));
+
+ expect(shadowRoot.activeElement).to.equal(shadowEl);
+ });
+ }
+ });
+});
diff --git a/packages/melody-idom/__tests__/PatchInnerSpec.ts b/packages/melody-idom/__tests__/PatchInnerSpec.ts
new file mode 100644
index 0000000..355d415
--- /dev/null
+++ b/packages/melody-idom/__tests__/PatchInnerSpec.ts
@@ -0,0 +1,181 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ patch,
+ patchInner,
+ elementOpen,
+ elementOpenStart,
+ elementOpenEnd,
+ elementClose,
+ elementVoid,
+ text,
+} from '../src';
+import { expect } from 'chai';
+
+describe("patching an element's children", () => {
+ let container;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ describe('with an existing document tree', () => {
+ let div;
+
+ function render() {
+ elementVoid('div', null, null, 'tabindex', '0');
+ }
+
+ beforeEach(function() {
+ div = document.createElement('div');
+ div.setAttribute('tabindex', '-1');
+ container.appendChild(div);
+ });
+
+ it('should preserve existing nodes', () => {
+ patchInner(container, render);
+ const child = container.childNodes[0];
+
+ expect(child).to.equal(div);
+ });
+
+ describe('should return DOM node', () => {
+ let node;
+
+ it('from elementOpen', () => {
+ patchInner(container, () => {
+ node = elementOpen('div');
+ elementClose('div');
+ });
+
+ expect(node).to.equal(div);
+ });
+
+ it('from elementClose', () => {
+ patchInner(container, () => {
+ elementOpen('div');
+ node = elementClose('div');
+ });
+
+ expect(node).to.equal(div);
+ });
+
+ it('from elementVoid', () => {
+ patchInner(container, () => {
+ node = elementVoid('div');
+ });
+
+ expect(node).to.equal(div);
+ });
+
+ it('from elementOpenEnd', () => {
+ patchInner(container, () => {
+ elementOpenStart('div');
+ node = elementOpenEnd('div');
+ elementClose('div');
+ });
+
+ expect(node).to.equal(div);
+ });
+ });
+ });
+
+ it('should be re-entrant', function() {
+ const containerOne = document.createElement('div');
+ const containerTwo = document.createElement('div');
+
+ function renderOne() {
+ elementOpen('div');
+ patchInner(containerTwo, renderTwo);
+ text('hello');
+ elementClose('div');
+ }
+
+ function renderTwo() {
+ text('foobar');
+ }
+
+ patchInner(containerOne, renderOne);
+
+ expect(containerOne.textContent).to.equal('hello');
+ expect(containerTwo.textContent).to.equal('foobar');
+ });
+
+ it('should pass third argument to render function', () => {
+ function render(content) {
+ const el = text(content);
+ }
+
+ patchInner(container, render, 'foobar');
+
+ expect(container.textContent).to.equal('foobar');
+ });
+
+ it('should patch a detached node', () => {
+ const container = document.createElement('div');
+ function render() {
+ elementVoid('span');
+ }
+
+ patchInner(container, render);
+
+ expect(container.firstChild.tagName).to.equal('SPAN');
+ });
+
+ it('should throw when an element is unclosed', function() {
+ expect(() => {
+ patch(container, () => {
+ elementOpen('div');
+ });
+ }).to.throw('One or more tags were not closed:\ndiv');
+ });
+});
+
+describe('patching a documentFragment', function() {
+ it('should create the required DOM nodes', function() {
+ const frag = document.createDocumentFragment();
+
+ patchInner(frag, function() {
+ elementOpen('div', null, null, 'id', 'aDiv');
+ elementClose('div');
+ });
+
+ expect(frag.childNodes[0].id).to.equal('aDiv');
+ });
+});
+
+describe('when patching an non existing element', function() {
+ it('should throw an error', function() {
+ expect(() =>
+ patchInner(null, function() {
+ expect(false).to.be.true;
+ }),
+ ).to.throw('Patch invoked without an element');
+ });
+});
+
+describe('patch', () => {
+ it('should alias patchInner', () => {
+ expect(patch).to.equal(patchInner);
+ });
+});
diff --git a/packages/melody-idom/__tests__/PatchOuterSpec.ts b/packages/melody-idom/__tests__/PatchOuterSpec.ts
new file mode 100644
index 0000000..15fee92
--- /dev/null
+++ b/packages/melody-idom/__tests__/PatchOuterSpec.ts
@@ -0,0 +1,238 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ patchOuter,
+ elementOpen,
+ elementClose,
+ elementVoid,
+ text,
+} from '../src';
+import { expect } from 'chai';
+
+describe('patching an element', () => {
+ let container;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ it('should update attributes', () => {
+ function render() {
+ elementVoid('div', null, null, 'tabindex', '0');
+ }
+
+ patchOuter(container, render);
+
+ expect(container.getAttribute('tabindex')).to.equal('0');
+ });
+
+ it('should return the DOM node', () => {
+ function render() {
+ elementVoid('div');
+ }
+
+ const result = patchOuter(container, render);
+
+ expect(result).to.equal(container);
+ });
+
+ it('should update children', () => {
+ function render() {
+ elementOpen('div');
+ elementVoid('span');
+ elementClose('div');
+ }
+
+ patchOuter(container, render);
+
+ expect(container.firstChild.tagName).to.equal('SPAN');
+ });
+
+ it('should be re-entrant', function() {
+ const containerOne = document.createElement('div');
+ const containerTwo = document.createElement('div');
+
+ function renderOne() {
+ elementOpen('div');
+ patchOuter(containerTwo, renderTwo);
+ text('hello');
+ elementClose('div');
+ }
+
+ function renderTwo() {
+ elementOpen('div');
+ text('foobar');
+ elementClose('div');
+ }
+
+ patchOuter(containerOne, renderOne);
+
+ expect(containerOne.textContent).to.equal('hello');
+ expect(containerTwo.textContent).to.equal('foobar');
+ });
+
+ it('should pass third argument to render function', () => {
+ function render(content) {
+ elementOpen('div');
+ text(content);
+ elementClose('div');
+ }
+
+ patchOuter(container, render, 'foobar');
+
+ expect(container.textContent).to.equal('foobar');
+ });
+
+ it('should patch a detached node', () => {
+ const container = document.createElement('div');
+ function render() {
+ elementOpen('div');
+ elementVoid('span');
+ elementClose('div');
+ }
+
+ patchOuter(container, render);
+
+ expect(container.firstChild.tagName).to.equal('SPAN');
+ });
+
+ describe('with an empty patch', () => {
+ let div;
+ let result;
+
+ beforeEach(() => {
+ div = container.appendChild(document.createElement('div'));
+
+ result = patchOuter(div, () => {});
+ });
+
+ it('should remove the DOM node on an empty patch', () => {
+ expect(container.firstChild).to.be.null;
+ });
+
+ it('should remove the DOM node on an empty patch', () => {
+ expect(result).to.be.null;
+ });
+ });
+
+ describe('with a different tag', () => {
+ describe('without a key', () => {
+ let div;
+ let span;
+ let result;
+
+ function render() {
+ elementVoid('span');
+ }
+
+ beforeEach(() => {
+ div = container.appendChild(document.createElement('div'));
+
+ result = patchOuter(div, render);
+ span = container.querySelector('span');
+ });
+
+ it('should replace the DOM node', () => {
+ expect(container.children).to.have.length(1);
+ expect(container.firstChild).to.equal(span);
+ });
+
+ it('should return the new DOM node', () => {
+ expect(result).to.equal(span);
+ });
+ });
+
+ describe('with a different key', () => {
+ let div;
+ let el;
+
+ function render(data) {
+ el = elementVoid(data.tag, data.key);
+ }
+
+ beforeEach(() => {
+ div = container.appendChild(document.createElement('div'));
+ });
+
+ it('should replace the DOM node when a key changes', () => {
+ div.setAttribute('key', 'key0');
+ patchOuter(div, render, { tag: 'span', key: 'key1' });
+ expect(container.children).to.have.length(1);
+ expect(container.firstChild).to.equal(el);
+ });
+
+ it('should replace the DOM node when a key is removed', () => {
+ div.setAttribute('key', 'key0');
+ patchOuter(div, render, { tag: 'span' });
+ expect(container.children).to.have.length(1);
+ expect(container.firstChild.tagName).to.equal('SPAN');
+ expect(container.firstChild).to.equal(el);
+ });
+
+ it('should replace the DOM node when a key is added', () => {
+ patchOuter(div, render, { tag: 'span', key: 'key2' });
+ expect(container.children).to.have.length(1);
+ expect(container.firstChild).to.equal(el);
+ });
+ });
+ });
+
+ it('should not hang on to removed elements with keys', () => {
+ function render() {
+ elementVoid('div', 'key');
+ }
+
+ const divOne = container.appendChild(document.createElement('div'));
+ patchOuter(divOne, render);
+ const el = container.firstChild;
+ patchOuter(el, () => {});
+ const divTwo = container.appendChild(document.createElement('div'));
+ patchOuter(divTwo, render);
+
+ expect(container.children).to.have.length(1);
+ expect(container.firstChild).to.not.equal(el);
+ });
+
+ it('should throw an error when patching too many elements', () => {
+ const div = container.appendChild(document.createElement('div'));
+ function render() {
+ elementVoid('div');
+ elementVoid('div');
+ }
+
+ expect(() => patchOuter(div, render)).to.throw(
+ 'There must be ' +
+ 'exactly one top level call corresponding to the patched element.',
+ );
+ });
+
+ describe('that does not exist', function() {
+ it('should throw an error', function() {
+ expect(() =>
+ patchOuter(null, function() {
+ expect(false).to.be.true;
+ }),
+ ).to.throw('Patch invoked without an element');
+ });
+ });
+});
diff --git a/packages/melody-idom/__tests__/RawSpec.ts b/packages/melody-idom/__tests__/RawSpec.ts
new file mode 100644
index 0000000..de413fe
--- /dev/null
+++ b/packages/melody-idom/__tests__/RawSpec.ts
@@ -0,0 +1,146 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { patch, raw, text, rawString, elementOpen, elementClose } from '../src';
+import { expect } from 'chai';
+
+describe('raw text nodes', () => {
+ let container;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ describe('when created', () => {
+ it('should render a text node with the specified value', () => {
+ patch(container, () => {
+ raw('hello ');
+ });
+ const node = container.childNodes[0];
+
+ expect(node.outerHTML).to.equal('hello ');
+ });
+ });
+
+ describe('when used as a marker', () => {
+ it('should render a text node with the specified value', () => {
+ patch(container, () => {
+ text(rawString('hello '));
+ });
+ const node = container.childNodes[0];
+
+ expect(node.outerHTML).to.equal('hello ');
+ });
+ });
+
+ describe('with conditional text', () => {
+ function render(data) {
+ raw(data);
+ }
+
+ it('should update the DOM when the text is updated', () => {
+ patch(container, () => render('Hello '));
+ patch(container, () => render('Hello World! '));
+ const node = container.childNodes[0];
+
+ expect(node.outerHTML).to.equal('Hello World! ');
+ });
+
+ it('should skip the DOM when the text is unchanged', () => {
+ patch(container, () => render('Hello '));
+ const oldNode = container.childNodes[0];
+ patch(container, () => render('Hello '));
+ const node = container.childNodes[0];
+ expect(node).to.equal(oldNode);
+ expect(node.outerHTML).to.equal('Hello ');
+ });
+ });
+
+ describe('with multi-element text', function() {
+ function render(data) {
+ raw(data);
+ }
+
+ it('should remove unnecessary elements', () => {
+ patch(container, () =>
+ render('Hello World
'),
+ );
+ expect(container.outerHTML).to.equal(
+ '',
+ );
+
+ patch(container, () => render('Hello World! '));
+ expect(container.outerHTML).to.equal(
+ 'Hello World!
',
+ );
+ });
+
+ it('should not replace elements if data is unchanged', () => {
+ patch(container, () =>
+ render('Hello World
'),
+ );
+ const firstChild = container.children[0];
+ const secondChild = container.children[1];
+ expect(container.outerHTML).to.equal(
+ '',
+ );
+
+ patch(container, () =>
+ render('Hello World
'),
+ );
+ expect(firstChild).to.equal(container.children[0]);
+ expect(secondChild).to.equal(container.children[1]);
+ expect(container.outerHTML).to.equal(
+ '',
+ );
+ });
+ });
+
+ describe('when dealing with empty text', function() {
+ function render(data) {
+ raw(data);
+ }
+
+ it('should not render anything', function() {
+ patch(container, () => render(''));
+ expect(container.outerHTML).to.equal('
');
+ });
+
+ it('should override the previous raw text', function() {
+ patch(container, () => render('Test '));
+ expect(container.outerHTML).to.equal(
+ 'Test
',
+ );
+
+ patch(container, () => render(''));
+ expect(container.outerHTML).to.equal('
');
+ });
+
+ it('should override the previously empty raw text', function() {
+ patch(container, () => render(''));
+ expect(container.outerHTML).to.equal('
');
+
+ patch(container, () => render('Test '));
+ expect(container.outerHTML).to.equal(
+ 'Test
',
+ );
+ });
+ });
+});
diff --git a/packages/melody-idom/__tests__/RefSpec.ts b/packages/melody-idom/__tests__/RefSpec.ts
new file mode 100644
index 0000000..daea50f
--- /dev/null
+++ b/packages/melody-idom/__tests__/RefSpec.ts
@@ -0,0 +1,118 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { patchOuter, elementOpen, elementClose, elementVoid } from '../src';
+
+describe('ref', () => {
+ const statics = ['class', 'refDiv'];
+ const template = _context => {
+ elementOpen('div');
+ if (_context.cond) {
+ elementOpen('section');
+ elementVoid('div', '1', statics, 'ref', _context.myRef);
+ elementClose('section');
+ }
+ elementClose('div');
+ };
+
+ it('should pass the element to the ref handler', () => {
+ const el = document.createElement('div');
+ const unsubscriber = jest.fn();
+ const myRef = jest.fn(el => ({ unsubscribe: unsubscriber }));
+ patchOuter(el, template, {
+ cond: true,
+ myRef,
+ });
+ expect(myRef).toHaveBeenCalledWith(el.querySelector('.refDiv'));
+ });
+
+ it('should pass the element after it has been inserted into the DOM to the handler', () => {
+ const el = document.createElement('div');
+ const unsubscriber = jest.fn();
+ const myRef = jest.fn(node => {
+ expect(node.parentNode.parentNode).toBe(el);
+ return { unsubscribe: unsubscriber };
+ });
+ patchOuter(el, template, {
+ cond: true,
+ myRef,
+ });
+ expect(myRef).toHaveBeenCalledWith(el.querySelector('.refDiv'));
+ });
+
+ it('should only invoke the ref handler once', () => {
+ const el = document.createElement('div');
+ const unsubscriber = jest.fn();
+ const myRef = jest.fn(el => ({ unsubscribe: unsubscriber }));
+ patchOuter(el, template, {
+ cond: true,
+ myRef,
+ });
+ patchOuter(el, template, {
+ cond: true,
+ myRef,
+ });
+ expect(myRef).toHaveBeenCalledTimes(1);
+ });
+
+ it('should invoke the unsubscriber when the element is removed', () => {
+ const el = document.createElement('div');
+ const unsubscriber = jest.fn();
+ const myRef = jest.fn(el => ({ unsubscribe: unsubscriber }));
+ patchOuter(el, template, {
+ cond: true,
+ myRef,
+ });
+ patchOuter(el, template, {
+ cond: false,
+ myRef,
+ });
+ expect(unsubscriber).toHaveBeenCalled();
+ });
+
+ it('should invoke the unsubscriber when a new ref handler is provided', () => {
+ const el = document.createElement('div');
+ const unsubscriber = jest.fn();
+ const unsubscriber2 = jest.fn();
+ const myRef = jest.fn(el => ({ unsubscribe: unsubscriber }));
+ const mySecondRef = jest.fn(el => ({ unsubscribe: unsubscriber2 }));
+ patchOuter(el, template, {
+ cond: true,
+ myRef,
+ });
+ patchOuter(el, template, {
+ cond: true,
+ myRef: mySecondRef,
+ });
+ expect(unsubscriber).toHaveBeenCalled();
+ expect(mySecondRef).toHaveBeenCalled();
+ });
+
+ it('should throw if a ref handler does not return an object with an unsubscriber function', () => {
+ const el = document.createElement('div');
+ expect(() => {
+ patchOuter(el, template, {
+ cond: true,
+ myRef: jest.fn(),
+ });
+ }).toThrow();
+ expect(() => {
+ patchOuter(el, template, {
+ cond: true,
+ myRef: jest.fn(el => ({ unsubscribe: 'not a function' })),
+ });
+ }).toThrow();
+ });
+});
diff --git a/packages/melody-idom/__tests__/SkipNodeSpec.ts b/packages/melody-idom/__tests__/SkipNodeSpec.ts
new file mode 100644
index 0000000..3c766bb
--- /dev/null
+++ b/packages/melody-idom/__tests__/SkipNodeSpec.ts
@@ -0,0 +1,58 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { patch, elementVoid, skipNode } from '../src';
+import { expect } from 'chai';
+
+describe('skip', () => {
+ let container;
+ let firstChild;
+ let lastChild;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ container.innerHTML = '
';
+
+ firstChild = container.firstChild;
+ lastChild = container.lastChild;
+
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ it('should keep nodes that were skipped at the start', () => {
+ patch(container, () => {
+ skipNode();
+ elementVoid('span');
+ });
+
+ expect(container.firstChild).to.equal(firstChild);
+ expect(container.lastChild).to.equal(lastChild);
+ });
+
+ it('should keep nodes that were skipped', () => {
+ patch(container, () => {
+ elementVoid('div');
+ skipNode();
+ });
+
+ expect(container.lastChild).to.equal(lastChild);
+ });
+});
diff --git a/packages/melody-idom/__tests__/SkipSpec.ts b/packages/melody-idom/__tests__/SkipSpec.ts
new file mode 100644
index 0000000..42ad730
--- /dev/null
+++ b/packages/melody-idom/__tests__/SkipSpec.ts
@@ -0,0 +1,102 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ patch,
+ elementOpen,
+ elementClose,
+ elementVoid,
+ skip,
+ text,
+} from '../src';
+import { expect } from 'chai';
+
+describe('skip', () => {
+ let container;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ function render(data) {
+ elementOpen('div');
+ if (data.skip) {
+ skip();
+ } else {
+ text('some ');
+ text('text');
+ }
+ elementClose('div');
+ }
+
+ it('should keep any DOM nodes in the subtree', () => {
+ patch(container, render, { skip: false });
+ patch(container, render, { skip: true });
+
+ expect(container.textContent).to.equal('some text');
+ });
+
+ it('should throw if an element is declared after skipping', () => {
+ expect(() => {
+ patch(container, () => {
+ skip();
+ elementOpen('div');
+ elementClose('div');
+ });
+ }).to.throw(
+ 'elementOpen() may not be called inside an element that has called skip().',
+ );
+ });
+
+ it('should throw if a text is declared after skipping', () => {
+ expect(() => {
+ patch(container, () => {
+ skip();
+ text('text');
+ });
+ }).to.throw(
+ 'text() may not be called inside an element that has called skip().',
+ );
+ });
+
+ it('should throw skip is called after declaring an element', () => {
+ expect(() => {
+ patch(container, () => {
+ elementVoid('div');
+ skip();
+ });
+ }).to.throw(
+ 'skip() must come before any child declarations inside the current element.',
+ );
+ });
+
+ it('should throw skip is called after declaring a text', () => {
+ expect(() => {
+ patch(container, () => {
+ text('text');
+ skip();
+ });
+ }).to.throw(
+ 'skip() must come before any child declarations inside the current element.',
+ );
+ });
+});
diff --git a/packages/melody-idom/__tests__/StylesSpec.ts b/packages/melody-idom/__tests__/StylesSpec.ts
new file mode 100644
index 0000000..bf33eea
--- /dev/null
+++ b/packages/melody-idom/__tests__/StylesSpec.ts
@@ -0,0 +1,126 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { patch, attributes, elementVoid } from '../src';
+import { expect } from 'chai';
+
+describe('style updates', () => {
+ let container;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ function browserSupportsCssCustomProperties() {
+ const style = document.createElement('div').style;
+ style.setProperty('--prop', 'value');
+ return style.getPropertyValue('--prop') === 'value';
+ }
+
+ function render(style) {
+ elementVoid('div', null, null, 'style', style);
+ }
+
+ it('should render with the correct style properties for objects', () => {
+ patch(container, () =>
+ render({
+ color: 'white',
+ backgroundColor: 'red',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.style.color).to.equal('white');
+ expect(el.style.backgroundColor).to.equal('red');
+ });
+
+ if (browserSupportsCssCustomProperties()) {
+ it('should apply custom properties', () => {
+ patch(container, () =>
+ render({
+ '--some-var': 'blue',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.style.getPropertyValue('--some-var')).to.equal('blue');
+ });
+ }
+
+ it('should support setProperty', function() {
+ const el = document.createElement('div');
+ el.style.setProperty('background-color', 'red');
+ expect(el.style.backgroundColor).to.equal('red');
+ });
+
+ it('should handle dashes in property names', () => {
+ patch(container, () =>
+ render({
+ 'background-color': 'red',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.style.backgroundColor).to.equal('red');
+ });
+
+ it('should update the correct style properties', () => {
+ patch(container, () =>
+ render({
+ color: 'white',
+ }),
+ );
+ patch(container, () =>
+ render({
+ color: 'red',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.style.color).to.equal('red');
+ });
+
+ it('should remove properties not present in the new object', () => {
+ patch(container, () =>
+ render({
+ color: 'white',
+ }),
+ );
+ patch(container, () =>
+ render({
+ backgroundColor: 'red',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.style.color).to.equal('');
+ expect(el.style.backgroundColor).to.equal('red');
+ });
+
+ it('should render with the correct style properties for strings', () => {
+ patch(container, () => render('color: white; background-color: red;'));
+ const el = container.childNodes[0];
+
+ expect(el.style.color).to.equal('white');
+ expect(el.style.backgroundColor).to.equal('red');
+ });
+});
diff --git a/packages/melody-idom/__tests__/TextNodesSpec.ts b/packages/melody-idom/__tests__/TextNodesSpec.ts
new file mode 100644
index 0000000..521a428
--- /dev/null
+++ b/packages/melody-idom/__tests__/TextNodesSpec.ts
@@ -0,0 +1,79 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { patch, text, elementOpenStart } from '../src';
+import { expect } from 'chai';
+
+describe('text nodes', () => {
+ let container;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ describe('when created', () => {
+ it('should render a text node with the specified value', () => {
+ patch(container, () => {
+ text('Hello world!');
+ });
+ const node = container.childNodes[0];
+
+ expect(node.textContent).to.equal('Hello world!');
+ expect(node).to.be.instanceof(Text);
+ });
+
+ it('should allow for multiple text nodes under one parent element', () => {
+ patch(container, () => {
+ text('Hello ');
+ text('World');
+ text('!');
+ });
+
+ expect(container.textContent).to.equal('Hello World!');
+ });
+
+ it('should throw when inside virtual attributes element', () => {
+ expect(() => {
+ patch(container, () => {
+ elementOpenStart('div');
+ text('Hello');
+ });
+ }).to.throw(
+ 'text() can not be called between elementOpenStart() and elementOpenEnd().',
+ );
+ });
+ });
+
+ describe('with conditional text', () => {
+ function render(data) {
+ text(data);
+ }
+
+ it('should update the DOM when the text is updated', () => {
+ patch(container, () => render('Hello'));
+ patch(container, () => render('Hello World!'));
+ const node = container.childNodes[0];
+
+ expect(node.textContent).to.equal('Hello World!');
+ });
+ });
+});
diff --git a/packages/melody-idom/__tests__/VirtualAttributesSpec.ts b/packages/melody-idom/__tests__/VirtualAttributesSpec.ts
new file mode 100644
index 0000000..3df27ee
--- /dev/null
+++ b/packages/melody-idom/__tests__/VirtualAttributesSpec.ts
@@ -0,0 +1,161 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ patch,
+ elementOpen,
+ elementOpenStart,
+ elementOpenEnd,
+ elementClose,
+ attr,
+} from '../src';
+import { expect } from 'chai';
+
+describe('virtual attribute updates', () => {
+ let container;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ });
+
+ describe('for conditional attributes', () => {
+ function render(obj) {
+ elementOpenStart('div', null, null, 'data-static', 'world');
+ if (obj.key) {
+ attr('data-expanded', obj.key);
+ }
+ elementOpenEnd();
+ elementClose('div');
+ }
+
+ it('should be present when specified', () => {
+ patch(container, () =>
+ render({
+ key: 'hello',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-expanded')).to.equal('hello');
+ });
+
+ it('should be not present when not specified', () => {
+ patch(container, () =>
+ render({
+ key: false,
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-expanded')).to.equal(null);
+ });
+
+ it('should update the DOM when they change', () => {
+ patch(container, () =>
+ render({
+ key: 'foo',
+ }),
+ );
+ patch(container, () =>
+ render({
+ key: 'bar',
+ }),
+ );
+ const el = container.childNodes[0];
+
+ expect(el.getAttribute('data-expanded')).to.equal('bar');
+ });
+
+ it('should include static attributes', function() {
+ patch(container, () =>
+ render({
+ key: false,
+ }),
+ );
+ const el = container.childNodes[0];
+ expect(el.getAttribute('data-static')).to.equal('world');
+ });
+
+ it('should throw when defined outside virtual attributes element', () => {
+ expect(() => {
+ patch(container, () => {
+ attr('data-expanded', true);
+ });
+ }).to.throw(
+ 'attr() can only be called after calling elementOpenStart().',
+ );
+ });
+ });
+
+ it('should throw when a virtual attributes element is unclosed', () => {
+ expect(() => {
+ patch(container, () => {
+ elementOpenStart('div');
+ });
+ }).to.throw(
+ 'elementOpenEnd() must be called after calling elementOpenStart().',
+ );
+ });
+
+ it('should throw when virtual attributes element is closed without being opened', () => {
+ expect(() => {
+ patch(container, () => {
+ elementOpenEnd('div');
+ });
+ }).to.throw(
+ 'elementOpenEnd() can only be called after calling elementOpenStart().',
+ );
+ });
+
+ it('should throw when opening an element inside a virtual attributes element', () => {
+ expect(() => {
+ patch(container, () => {
+ elementOpenStart('div');
+ elementOpen('div');
+ });
+ }).to.throw(
+ 'elementOpen() can not be called between elementOpenStart() and elementOpenEnd().',
+ );
+ });
+
+ it('should throw when opening a virtual attributes element inside a virtual attributes element', () => {
+ expect(() => {
+ patch(container, () => {
+ elementOpenStart('div');
+ elementOpenStart('div');
+ });
+ }).to.throw(
+ 'elementOpenStart() can not be called between elementOpenStart() and elementOpenEnd().',
+ );
+ });
+
+ it('should throw when closing an element inside a virtual attributes element', () => {
+ expect(() => {
+ patch(container, () => {
+ elementOpenStart('div');
+ elementClose('div');
+ });
+ }).to.throw(
+ 'elementClose() can not be called between elementOpenStart() and elementOpenEnd().',
+ );
+ });
+});
diff --git a/packages/melody-idom/package.json b/packages/melody-idom/package.json
new file mode 100644
index 0000000..a68772d
--- /dev/null
+++ b/packages/melody-idom/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "melody-idom",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./built/index.js",
+ "types": "./built/index.d.ts",
+ "scripts": {
+ "prebuild": "tsc",
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i built/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash": "^4.12.0"
+ },
+ "devDependencies": {
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-idom/src/assertions.ts b/packages/melody-idom/src/assertions.ts
new file mode 100644
index 0000000..09f72c9
--- /dev/null
+++ b/packages/melody-idom/src/assertions.ts
@@ -0,0 +1,213 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Keeps track whether or not we are in an attributes declaration (after
+ * elementOpenStart, but before elementOpenEnd).
+ * @type {boolean}
+ */
+var inAttributes = false;
+
+/**
+ * Keeps track whether or not we are in an element that should not have its
+ * children cleared.
+ * @type {boolean}
+ */
+var inSkip = false;
+
+/**
+ * Makes sure that there is a current patch context.
+ * @param {*} context
+ */
+var assertInPatch = function(functionName, context) {
+ if (!context) {
+ throw new Error('Cannot call ' + functionName + '() unless in patch');
+ }
+};
+
+/**
+ * Makes sure that a patch closes every node that it opened.
+ * @param {?Node} openElement
+ * @param {!Node|!DocumentFragment} root
+ */
+var assertNoUnclosedTags = function(openElement, root) {
+ if (openElement === root) {
+ return;
+ }
+
+ var currentElement = openElement;
+ var openTags = [];
+ while (currentElement && currentElement !== root) {
+ openTags.push(currentElement.nodeName.toLowerCase());
+ currentElement = currentElement.parentNode;
+ }
+
+ throw new Error(
+ 'One or more tags were not closed:\n' + openTags.join('\n'),
+ );
+};
+
+/**
+ * Makes sure that the caller is not where attributes are expected.
+ * @param {string} functionName
+ */
+var assertNotInAttributes = function(functionName) {
+ if (inAttributes) {
+ throw new Error(
+ functionName +
+ '() can not be called between ' +
+ 'elementOpenStart() and elementOpenEnd().',
+ );
+ }
+};
+
+/**
+ * Makes sure that the caller is not inside an element that has declared skip.
+ * @param {string} functionName
+ */
+var assertNotInSkip = function(functionName) {
+ if (inSkip) {
+ throw new Error(
+ functionName +
+ '() may not be called inside an element ' +
+ 'that has called skip().',
+ );
+ }
+};
+
+/**
+ * Makes sure that the caller is where attributes are expected.
+ * @param {string} functionName
+ */
+var assertInAttributes = function(functionName) {
+ if (!inAttributes) {
+ throw new Error(
+ functionName +
+ '() can only be called after calling ' +
+ 'elementOpenStart().',
+ );
+ }
+};
+
+/**
+ * Makes sure the patch closes virtual attributes call
+ */
+var assertVirtualAttributesClosed = function() {
+ if (inAttributes) {
+ throw new Error(
+ 'elementOpenEnd() must be called after calling ' +
+ 'elementOpenStart().',
+ );
+ }
+};
+
+/**
+ * Makes sure that tags are correctly nested.
+ * @param {string} nodeName
+ * @param {string} tag
+ */
+var assertCloseMatchesOpenTag = function(nodeName, tag) {
+ if (nodeName !== tag) {
+ throw new Error(
+ 'Received a call to close "' +
+ tag +
+ '" but "' +
+ nodeName +
+ '" was open.',
+ );
+ }
+};
+
+/**
+ * Makes sure that no children elements have been declared yet in the current
+ * element.
+ * @param {string} functionName
+ * @param {?Node} previousNode
+ */
+var assertNoChildrenDeclaredYet = function(functionName, previousNode) {
+ if (previousNode !== null) {
+ throw new Error(
+ functionName +
+ '() must come before any child ' +
+ 'declarations inside the current element.',
+ );
+ }
+};
+
+/**
+ * Checks that a call to patchOuter actually patched the element.
+ * @param {?Node} node The node requested to be patched.
+ * @param {?Node} previousNode The previousNode after the patch.
+ */
+var assertPatchElementNoExtras = function(
+ startNode,
+ currentNode,
+ expectedNextNode,
+ expectedPrevNode,
+) {
+ const wasUpdated =
+ currentNode.nextSibling === expectedNextNode &&
+ currentNode.previousSibling === expectedPrevNode;
+ const wasChanged =
+ currentNode.nextSibling === startNode.nextSibling &&
+ currentNode.previousSibling === expectedPrevNode;
+ const wasRemoved = currentNode === startNode;
+
+ if (!wasUpdated && !wasChanged && !wasRemoved) {
+ throw new Error(
+ 'There must be exactly one top level call corresponding ' +
+ 'to the patched element.',
+ );
+ }
+};
+
+/**
+ * Updates the state of being in an attribute declaration.
+ * @param {boolean} value
+ * @return {boolean} the previous value.
+ */
+var setInAttributes = function(value) {
+ var previous = inAttributes;
+ inAttributes = value;
+ return previous;
+};
+
+/**
+ * Updates the state of being in a skip element.
+ * @param {boolean} value
+ * @return {boolean} the previous value.
+ */
+var setInSkip = function(value) {
+ var previous = inSkip;
+ inSkip = value;
+ return previous;
+};
+
+/** */
+export {
+ assertInPatch,
+ assertNoUnclosedTags,
+ assertNotInAttributes,
+ assertInAttributes,
+ assertCloseMatchesOpenTag,
+ assertVirtualAttributesClosed,
+ assertNoChildrenDeclaredYet,
+ assertNotInSkip,
+ assertPatchElementNoExtras,
+ setInAttributes,
+ setInSkip,
+};
diff --git a/packages/melody-idom/src/attributes.ts b/packages/melody-idom/src/attributes.ts
new file mode 100644
index 0000000..9adeb99
--- /dev/null
+++ b/packages/melody-idom/src/attributes.ts
@@ -0,0 +1,174 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { getData } from './node_data';
+
+const getNamespace = function(name) {
+ if (name.lastIndexOf('xml:', 0) === 0) {
+ return 'http://www.w3.org/XML/1998/namespace';
+ }
+
+ if (name.lastIndexOf('xlink:', 0) === 0) {
+ return 'http://www.w3.org/1999/xlink';
+ }
+};
+
+/**
+ * Applies an attribute or property to a given Element. If the value is null
+ * or undefined, it is removed from the Element. Otherwise, the value is set
+ * as an attribute.
+ * @param {!Element} el
+ * @param {string} name The attribute's name.
+ * @param {?(boolean|number|string)=} value The attribute's value.
+ */
+const applyAttr = function(el, name, value) {
+ if (value == null) {
+ el.removeAttribute(name);
+ } else {
+ const attrNS = getNamespace(name);
+ if (attrNS) {
+ el.setAttributeNS(attrNS, name, value);
+ } else {
+ el.setAttribute(name, value);
+ }
+ }
+};
+
+/**
+ * Updates a single attribute on an Element.
+ * @param {!Element} el
+ * @param {string} name The attribute's name.
+ * @param {*} value The attribute's value. If the value is an object or
+ * function it is set on the Element, otherwise, it is set as an HTML
+ * attribute.
+ */
+function applyAttributeTyped(el, name, value) {
+ var type = typeof value;
+
+ if (type === 'object' || type === 'function') {
+ setProperty(el, name, value);
+ } else {
+ applyAttr(el, name /** @type {?(boolean|number|string)} */, value);
+ }
+}
+
+function setProperty(el, name, value) {
+ try {
+ el[name] = value;
+ } catch (e) {}
+}
+
+function eventProxy(e) {
+ return this._listeners[e.type](e);
+}
+
+/**
+ * Calls the appropriate attribute mutator for this attribute.
+ * @param {!Element} el
+ * @param {string} name The attribute's name.
+ * @param {*} value The attribute's value.
+ */
+const updateAttribute = function(el, name, value) {
+ var data = getData(el);
+ var attrs = data.attrs;
+
+ if (attrs[name] === value) {
+ return;
+ }
+
+ if (name === 'style') {
+ const old = attrs.style;
+ if (!value || typeof value === 'string') {
+ el.style.cssText = value || '';
+ } else {
+ if (typeof old === 'string') {
+ el.style.cssText = '';
+ } else {
+ for (let i in old) {
+ if (!(i in value)) {
+ el.style[i] = '';
+ }
+ }
+ }
+ for (let i in value) {
+ if (i.indexOf('-') >= 0) {
+ el.style.setProperty(i, value[i]);
+ } else {
+ el.style[i] = value[i];
+ }
+ }
+ }
+ } else if (name === 'ref') {
+ const old = attrs.ref;
+ if (old) {
+ if (old.creator === value) {
+ return;
+ }
+ old.disposer.unsubscribe();
+ }
+ if (!value) {
+ attrs.ref = null;
+ return;
+ }
+ attrs.ref = {
+ creator: value,
+ disposer: value(el),
+ };
+ if (process.env.NODE_ENV !== 'production') {
+ if (
+ !attrs.ref.disposer ||
+ typeof attrs.ref.disposer.unsubscribe !== 'function'
+ ) {
+ throw new Error(
+ `A ref handler is supposed to return a Subscription object which must have a "unsubscribe" method.`,
+ );
+ }
+ }
+ return;
+ } else if (name[0] === 'o' && name[1] === 'n') {
+ if (typeof value === 'function') {
+ let useCapture = name !== (name = name.replace(/Capture$/, ''));
+ name = name.toLowerCase().substring(2);
+ if (value) {
+ if (!attrs[name])
+ el.addEventListener(name, eventProxy, useCapture);
+ } else {
+ el.removeEventListener(name, eventProxy, useCapture);
+ }
+ (el._listeners || (el._listeners = {}))[name] = value;
+ } else {
+ applyAttributeTyped(el, name, value);
+ }
+ } else if (
+ name !== 'list' &&
+ name !== 'type' &&
+ !(el.ownerSVGElement || el.localName === 'svg') &&
+ name in el
+ ) {
+ setProperty(el, name, value == null ? '' : value);
+ if (value == null || value === false) {
+ el.removeAttribute(name);
+ }
+ } else {
+ applyAttributeTyped(el, name, value);
+ }
+
+ attrs[name] = value;
+};
+
+/** */
+export { updateAttribute };
diff --git a/packages/melody-idom/src/core.ts b/packages/melody-idom/src/core.ts
new file mode 100644
index 0000000..c5187e0
--- /dev/null
+++ b/packages/melody-idom/src/core.ts
@@ -0,0 +1,648 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { createElement, createText, createRaw } from './nodes';
+import { getData } from './node_data';
+import {
+ assertInPatch,
+ assertNoUnclosedTags,
+ assertNotInAttributes,
+ assertVirtualAttributesClosed,
+ assertNoChildrenDeclaredYet,
+ assertPatchElementNoExtras,
+ setInAttributes,
+ setInSkip,
+} from './assertions';
+import { getFocusedPath, moveBefore } from './dom_util';
+import { unmountComponent } from './util';
+import { enqueueComponent } from './renderQueue';
+import { link, reset } from './hierarchy';
+
+type Class = { new (): T };
+
+export interface CallableComponent {
+ apply(props: any): void;
+ el: Element;
+}
+
+export interface RenderableComponent {
+ el: Element;
+ refs: any;
+ render(): void;
+ notify(): void;
+}
+
+/** @type {?Node} */
+var currentNode;
+
+/** @type {?Node} */
+var currentParent;
+
+/** @type {?Document} */
+var doc;
+
+var componentKey = null;
+var currentComponent = null;
+
+var deletedNodes: Array = null;
+
+const markFocused = function(focusPath: Array, focused: boolean): void {
+ for (let i = 0; i < focusPath.length; i += 1) {
+ getData(focusPath[i]).focused = focused;
+ }
+};
+
+const patchFactory = function(run) {
+ return function(node, fn, data) {
+ if (process.env.NODE_ENV !== 'production') {
+ if (!node) {
+ throw new Error('Patch invoked without an element.');
+ }
+ }
+ const prevDeletedNodes = deletedNodes;
+ const prevDoc = doc;
+ const prevCurrentNode = currentNode;
+ const prevCurrentParent = currentParent;
+ const prevCurrentComponent = currentComponent;
+
+ let previousInAttribute = false;
+ let previousInSkip = false;
+
+ deletedNodes = [];
+ doc = node.ownerDocument;
+ currentParent = node.parentNode;
+
+ if (process.env.NODE_ENV !== 'production') {
+ previousInAttribute = setInAttributes(false);
+ previousInSkip = setInSkip(false);
+ }
+
+ const focusPath = getFocusedPath(node, currentParent);
+ markFocused(focusPath, true);
+ let retVal;
+ if (process.env.NODE_ENV !== 'production') {
+ try {
+ retVal = run(node, fn, data);
+ } catch (e) {
+ // reset context
+ deletedNodes = prevDeletedNodes;
+ doc = prevDoc;
+ currentNode = prevCurrentNode;
+ currentParent = prevCurrentParent;
+ currentComponent = prevCurrentComponent;
+ // rethrow the error
+ throw e;
+ }
+ } else {
+ retVal = run(node, fn, data);
+ }
+
+ markFocused(focusPath, false);
+
+ if (process.env.NODE_ENV !== 'production') {
+ assertVirtualAttributesClosed();
+ setInAttributes(previousInAttribute);
+ setInSkip(previousInSkip);
+ }
+
+ var i, len;
+ for (i = 0, len = deletedNodes.length; i < len; i++) {
+ nodeDeleted(deletedNodes[i]);
+ }
+
+ // reset context
+ deletedNodes = prevDeletedNodes;
+ doc = prevDoc;
+ currentNode = prevCurrentNode;
+ currentParent = prevCurrentParent;
+ currentComponent = prevCurrentComponent;
+
+ return retVal;
+ };
+};
+
+function nodeDeleted(node) {
+ const data = getData(node);
+ if (data.attrs.ref && data.attrs.ref.disposer) {
+ data.attrs.ref.disposer.unsubscribe();
+ data.attrs.ref = null;
+ }
+ if (data.componentInstance) {
+ unmountComponent(data.componentInstance);
+ }
+ // not an ideal solution but we can eventually move it
+ // towards a scheduler (perhaps `requestIdleCallback` if we notice
+ // that there are actual issues with this)
+ // Chose a recursive solution here to avoid unnecessary memory usage
+ let child = node.firstChild;
+ while (child) {
+ nodeDeleted(child);
+ child = child.nextSibling;
+ }
+}
+
+const patchInner = patchFactory(function(node, fn, data) {
+ currentNode = node;
+ enterNode();
+ fn(data);
+ exitNode();
+
+ if (process.env.NODE_ENV !== 'production') {
+ assertNoUnclosedTags(currentNode, node);
+ }
+
+ return node;
+});
+
+const patchOuter = patchFactory(function(node, fn, data) {
+ const startNode = { nextSibling: node };
+ let expectedNextNode = null;
+ let expectedPrevNode = null;
+
+ if (process.env.NODE_ENV !== 'production') {
+ expectedNextNode = node.nextSibling;
+ expectedPrevNode = node.previousSibling;
+ }
+
+ currentNode = startNode;
+ fn(data);
+
+ if (process.env.NODE_ENV !== 'production') {
+ assertPatchElementNoExtras(
+ startNode,
+ currentNode,
+ expectedNextNode,
+ expectedPrevNode,
+ );
+ }
+
+ if (node !== currentNode && node.parentNode) {
+ removeChild(currentParent, node, getData(currentParent).keyMap);
+ }
+
+ return startNode === currentNode ? null : currentNode;
+});
+
+/**
+ * Checks whether or not the current node matches the specified nodeName and
+ * key.
+ *
+ * @param {?string} nodeName The nodeName for this node.
+ * @param {?string=} key An optional key that identifies a node.
+ * @return {boolean} True if the node matches, false otherwise.
+ */
+var matches = function(
+ matchNode: Node,
+ nodeName: string,
+ key?: string,
+): boolean {
+ var data = getData(matchNode);
+
+ // Key check is done using double equals as we want to treat a null key the
+ // same as undefined. This should be okay as the only values allowed are
+ // strings, null and undefined so the == semantics are not too weird.
+ // templates rendered on the server side may not have keys at all while melody templates
+ // always will have them so we reconcile the dom in those cases.
+ if (nodeName === data.nodeName) {
+ if (key == data.key) {
+ return true;
+ }
+ // exisiting DOM element does not have a key
+ // which means we can hook onto it freely
+ if (!data.key) {
+ data.key = key;
+ // but we'll need to update the parent element
+ const parentKeys = currentParent && getData(currentParent).keyMap;
+ if (parentKeys) {
+ parentKeys[key] = matchNode;
+ }
+ return true;
+ }
+ }
+ return false;
+};
+
+/**
+ * Aligns the virtual Element definition with the actual DOM, moving the
+ * corresponding DOM node to the correct location or creating it if necessary.
+ * @param {string} nodeName For an Element, this should be a valid tag string.
+ * For a Text, this should be #text.
+ * @param {?string=} key The key used to identify this element.
+ * @param {?Array<*>=} statics For an Element, this should be an array of
+ * name-value pairs.
+ */
+var alignWithDOM = function(nodeName: string, key?: string): void {
+ if (currentNode && matches(currentNode, nodeName, key)) {
+ return;
+ }
+
+ const parentData = getData(currentParent);
+ const currentNodeData = currentNode && getData(currentNode);
+ const keyMap = parentData.keyMap;
+ let fromKeyMap = false;
+ let node;
+ let componentInstance = null;
+
+ // Check to see if the node has moved within the parent.
+ if (key) {
+ const keyNode = keyMap[key];
+ if (keyNode) {
+ if (matches(keyNode, nodeName, key)) {
+ fromKeyMap = true;
+ node = keyNode;
+ } else if (keyNode === currentNode) {
+ const keyNodeData = getData(keyNode);
+ // if (keyNodeData.componentInstance === currentComponent) {
+ if (keyNodeData.componentInstance) {
+ componentInstance = keyNodeData.componentInstance;
+ keyNodeData.componentInstance = null;
+ } else {
+ deletedNodes.push(keyNode);
+ }
+ } else {
+ removeChild(currentParent, keyNode, keyMap);
+ }
+ } else if (
+ currentNode &&
+ currentNode.nodeType === 3 &&
+ currentNode.data.trim() === ''
+ ) {
+ // special handling here to ignore empty text nodes if the one after it is what we're actually looking for
+ // this reduces a lot of special handling for server side rendered content.
+ if (
+ currentNode.nextSibling &&
+ matches(currentNode.nextSibling, nodeName, key)
+ ) {
+ node = currentNode.nextSibling;
+ }
+ }
+ }
+
+ // Create the node if it doesn't exist.
+ if (!node) {
+ if (nodeName === '#text') {
+ node = createText(doc);
+ } else {
+ node = createElement(doc, currentParent, nodeName, key);
+ }
+
+ if (key) {
+ keyMap[key] = node;
+ }
+ }
+
+ if (componentInstance) {
+ getData(node).componentInstance = componentInstance;
+ componentInstance.el = node;
+ }
+
+ // Re-order the node into the right position, preserving focus if either
+ // node or currentNode are focused by making sure that they are not detached
+ // from the DOM.
+ if (getData(node).focused) {
+ // move everything else before the node.
+ moveBefore(currentParent, node, currentNode);
+ } else if (
+ !(fromKeyMap && !node.parentNode) &&
+ currentNodeData &&
+ currentNodeData.key &&
+ !currentNodeData.focused
+ ) {
+ // Remove the currentNode, which can always be added back since we hold a
+ // reference through the keyMap. This prevents a large number of moves when
+ // a keyed item is removed or moved backwards in the DOM.
+ currentParent.replaceChild(node, currentNode);
+ parentData.keyMapValid = false;
+ } else if (
+ currentNode &&
+ currentNode.nextSibling === node &&
+ currentNode.nodeType === 3 &&
+ currentNode.data.trim() === ''
+ ) {
+ // if the empty text node handling above was successful, we simply remove the skipped text node
+ currentParent.removeChild(currentNode);
+ } else {
+ currentParent.insertBefore(node, currentNode);
+ }
+
+ currentNode = node;
+};
+
+const removeChild = function(node: Node, child: Node, keyMap): void {
+ node.removeChild(child);
+ deletedNodes.push(child);
+
+ const key = getData(child).key;
+ if (key) {
+ delete keyMap[key];
+ }
+};
+
+/**
+ * Clears out any unvisited Nodes, as the corresponding virtual element
+ * functions were never called for them.
+ */
+var clearUnvisitedDOM = function(): void {
+ var node = currentParent;
+ var data = getData(node);
+ var keyMap = data.keyMap;
+ var keyMapValid = data.keyMapValid;
+ var child = node.lastChild;
+ var key;
+
+ if (child === currentNode && keyMapValid) {
+ return;
+ }
+
+ while (child && child !== currentNode) {
+ removeChild(node, child, keyMap);
+ child = node.lastChild;
+ }
+
+ // Clean the keyMap, removing any unusued keys.
+ if (!keyMapValid) {
+ for (key in keyMap) {
+ child = keyMap[key];
+ if (child.parentNode !== node) {
+ deletedNodes.push(child);
+ delete keyMap[key];
+ }
+ }
+
+ data.keyMapValid = true;
+ }
+};
+
+/**
+ * Changes to the first child of the current node.
+ */
+var enterNode = function(): void {
+ currentParent = currentNode;
+ currentNode = null;
+};
+
+/**
+ * Changes to the next sibling of the current node.
+ */
+var nextNode = function(): void {
+ currentNode = getNextNode();
+};
+
+var getNextNode = function(): Node | null {
+ if (currentNode) {
+ return currentNode.nextSibling;
+ } else {
+ return currentParent.firstChild;
+ }
+};
+
+/**
+ * Changes to the parent of the current node, removing any unvisited children.
+ */
+var exitNode = function(): void {
+ clearUnvisitedDOM();
+
+ currentNode = currentParent;
+ currentParent = currentParent.parentNode;
+};
+
+var updateComponent = function(comp: RenderableComponent): void {
+ const data = getData(comp.el);
+ const parentComponent = currentComponent;
+ componentKey = data.key;
+
+ reset(comp);
+
+ currentComponent = comp;
+ comp.render();
+
+ currentComponent = parentComponent;
+};
+
+var scheduleComponent = function(
+ Component: Class | CallableComponent,
+ key: string,
+ props: any,
+ el?: Node,
+): any {
+ var comp;
+ if (el) {
+ // we've already seen this component
+ var data = getData(el);
+ comp = data.componentInstance;
+ if (!comp) {
+ // but apparently we didn't have a component instance so far
+ // most likely we're mounting a server side rendered DOM
+ comp =
+ typeof Component === 'function' ? new Component() : Component;
+ comp.el = el;
+ data.componentInstance = comp;
+ }
+ // Q: Do we even want to support this in the future?
+ // if (typeof Component === 'function' && !(comp instanceof Component)) {
+ // unmountComponent(comp);
+ // comp = null;
+ // }
+ elementOpen(data.nodeName, key);
+ skip();
+ elementClose();
+ } else {
+ // unknown component
+ if (typeof Component === 'function') {
+ comp = new Component();
+ } else {
+ comp = Component;
+ }
+
+ elementOpen('m-placeholder', key);
+ skip();
+ comp.el = elementClose();
+ getData(comp.el).componentInstance = comp;
+ }
+
+ if (currentComponent) {
+ link(currentComponent, comp);
+ }
+ return comp.apply(props);
+};
+
+var component = function(
+ Component: Class | CallableComponent,
+ key: string,
+ props: any,
+): any {
+ var el = getData(currentParent).keyMap[key];
+ return scheduleComponent(Component, key, props, el);
+};
+
+var getCurrentComponent = function(): RenderableComponent {
+ return currentComponent;
+};
+
+var mount = function(
+ element: Node,
+ Component: Class | CallableComponent,
+ props: any,
+): any {
+ var data = getData(element);
+ var key = data && data.key;
+ var comp = data.componentInstance;
+ var isComponentInstance = typeof Component !== 'function';
+ // if the existing component is not an instance of the specified component type
+ // then we just unmount the existing one and proceed as if none ever existed
+ if (
+ comp &&
+ !isComponentInstance &&
+ !(comp instanceof (Component as Class))
+ ) {
+ unmountComponent(comp);
+ }
+ return scheduleComponent(Component, key, props, element);
+};
+
+/**
+ * Makes sure that the current node is an Element with a matching tagName and
+ * key.
+ *
+ * @param {string} tag The element's tag.
+ * @param {?string=} key The key used to identify this element. This can be an
+ * empty string, but performance may be better if a unique value is used
+ * when iterating over an array of items.
+ * @return {!Element} The corresponding Element.
+ */
+var elementOpen = function(tag: string, key?: string) {
+ nextNode();
+ alignWithDOM(tag, componentKey || key);
+ componentKey = null;
+ enterNode();
+ return currentParent;
+};
+
+/**
+ * Closes the currently open Element, removing any unvisited children if
+ * necessary.
+ *
+ * @return {!Element} The corresponding Element.
+ */
+var elementClose = function(): void {
+ if (process.env.NODE_ENV !== 'production') {
+ setInSkip(false);
+ }
+
+ exitNode();
+ return currentNode;
+};
+
+/**
+ * Makes sure the current node is a Text node and creates a Text node if it is
+ * not.
+ *
+ * @return {!Text} The corresponding Text Node.
+ */
+var text = function(): Text {
+ nextNode();
+ alignWithDOM('#text', null);
+ return currentNode;
+};
+
+/**
+ * Gets the current Element being patched.
+ * @return {!Element}
+ */
+var currentElement = function(): Node {
+ if (process.env.NODE_ENV !== 'production') {
+ assertInPatch('currentElement', deletedNodes);
+ assertNotInAttributes('currentElement');
+ }
+ return currentParent;
+};
+
+/**
+ * Skips the children in a subtree, allowing an Element to be closed without
+ * clearing out the children.
+ */
+var skip = function(): void {
+ if (process.env.NODE_ENV !== 'production') {
+ assertNoChildrenDeclaredYet('skip', currentNode);
+ setInSkip(true);
+ }
+ currentNode = currentParent.lastChild;
+};
+
+var inPatch = function(): boolean {
+ return !!deletedNodes;
+};
+
+const skipNode = nextNode;
+
+var insertRawHtml = function(html: string): void {
+ var children = createRaw(doc, html);
+ var node = doc.createDocumentFragment(),
+ lastChild = children[children.length - 1];
+ while (children.length) {
+ node.appendChild(children[0]);
+ }
+ currentParent.insertBefore(node, currentNode);
+ currentNode = lastChild;
+};
+
+var raw = function(html: string): void {
+ nextNode();
+ if (currentNode && matches(currentNode, '#raw', null)) {
+ // patch node
+ var data = getData(currentNode),
+ remainingSiblingCount = data.childLength - 1;
+ if (data.text !== html) {
+ // if the text is not the same as before, we'll have some work to do
+ insertRawHtml(html);
+ // remove the remaining siblings of the old child
+ if (data.childLength > 1) {
+ while (remainingSiblingCount--) {
+ currentParent.removeChild(currentNode.nextSibling);
+ }
+ }
+ } else if (remainingSiblingCount) {
+ // still the same text so just jump over the remaining siblings
+ while (remainingSiblingCount--) {
+ currentNode = currentNode.nextSibling;
+ }
+ }
+ } else {
+ // insert raw html
+ insertRawHtml(html);
+ }
+
+ return currentNode;
+};
+
+/** */
+export {
+ elementOpen,
+ elementClose,
+ text,
+ patchInner,
+ patchOuter,
+ currentElement,
+ skip,
+ skipNode,
+ inPatch,
+ raw,
+ mount,
+ component,
+ getCurrentComponent as currentComponent,
+ updateComponent,
+ enqueueComponent,
+};
diff --git a/packages/melody-idom/src/dom_util.ts b/packages/melody-idom/src/dom_util.ts
new file mode 100644
index 0000000..4337e3b
--- /dev/null
+++ b/packages/melody-idom/src/dom_util.ts
@@ -0,0 +1,108 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @param {!Node} node
+ * @return {boolean} True if the node the root of a document, false otherwise.
+ */
+const isDocumentRoot = function(node) {
+ // For ShadowRoots, check if they are a DocumentFragment instead of if they
+ // are a ShadowRoot so that this can work in 'use strict' if ShadowRoots are
+ // not supported.
+ return node instanceof Document || node instanceof DocumentFragment;
+};
+
+/**
+ * @param {!Node} node The node to start at, inclusive.
+ * @param {?Node} root The root ancestor to get until, exclusive.
+ * @return {!Array} The ancestry of DOM nodes.
+ */
+const getAncestry = function(node, root) {
+ const ancestry = [];
+ let cur = node;
+
+ while (cur !== root) {
+ ancestry.push(cur);
+ cur = cur.parentNode;
+ }
+
+ return ancestry;
+};
+
+/**
+ * @param {!Node} node
+ * @return {!Node} The root node of the DOM tree that contains node.
+ */
+const getRoot = function(node) {
+ let cur = node;
+ let prev = cur;
+
+ while (cur) {
+ prev = cur;
+ cur = cur.parentNode;
+ }
+
+ return prev;
+};
+
+/**
+ * @param {!Node} node The node to get the activeElement for.
+ * @return {?Element} The activeElement in the Document or ShadowRoot
+ * corresponding to node, if present.
+ */
+const getActiveElement = function(node) {
+ const root = getRoot(node);
+ return isDocumentRoot(root) ? root.activeElement : null;
+};
+
+/**
+ * Gets the path of nodes that contain the focused node in the same document as
+ * a reference node, up until the root.
+ * @param {!Node} node The reference node to get the activeElement for.
+ * @param {?Node} root The root to get the focused path until.
+ * @return {!Array}
+ */
+const getFocusedPath = function(node, root) {
+ const activeElement = getActiveElement(node);
+
+ if (!activeElement || !node.contains(activeElement)) {
+ return [];
+ }
+
+ return getAncestry(activeElement, root);
+};
+
+/**
+ * Like insertBefore, but instead instead of moving the desired node, instead
+ * moves all the other nodes after.
+ * @param {?Node} parentNode
+ * @param {!Node} node
+ * @param {?Node} referenceNode
+ */
+const moveBefore = function(parentNode, node, referenceNode) {
+ const insertReferenceNode = node.nextSibling;
+ let cur = referenceNode;
+
+ while (cur && cur !== node) {
+ const next = cur.nextSibling;
+ parentNode.insertBefore(cur, insertReferenceNode);
+ cur = next;
+ }
+};
+
+/** */
+export { getFocusedPath, moveBefore };
diff --git a/packages/melody-idom/src/hierarchy.ts b/packages/melody-idom/src/hierarchy.ts
new file mode 100644
index 0000000..05a8f69
--- /dev/null
+++ b/packages/melody-idom/src/hierarchy.ts
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const parentToChildren = new WeakMap();
+const childToParent = new WeakMap();
+
+export function link(parent, child) {
+ childToParent.set(child, parent);
+ const children = getChildren(parent);
+ children.push(child);
+}
+
+export function unlink(node) {
+ parentToChildren.delete(node);
+ childToParent.delete(node);
+}
+
+export function getChildren(parent) {
+ let children = parentToChildren.get(parent);
+ if (!children) {
+ children = [];
+ parentToChildren.set(parent, children);
+ }
+ return children;
+}
+
+export function getParent(child) {
+ return childToParent.get(child);
+}
+
+export function reset(node) {
+ parentToChildren.set(node, []);
+}
diff --git a/packages/melody-idom/src/index.ts b/packages/melody-idom/src/index.ts
new file mode 100644
index 0000000..80bf733
--- /dev/null
+++ b/packages/melody-idom/src/index.ts
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import options from './options';
+export { options };
+export { flush, Deadline, clear as clearQueue } from './renderQueue';
+export { getParent, link } from './hierarchy';
+export {
+ patchInner as patch,
+ patchInner,
+ patchOuter,
+ currentElement,
+ skip,
+ skipNode,
+ mount,
+ component,
+ enqueueComponent,
+ RenderableComponent,
+ CallableComponent,
+} from './core';
+export {
+ elementVoid,
+ elementOpenStart,
+ elementOpenEnd,
+ elementOpen,
+ elementClose,
+ text,
+ attr,
+ raw,
+ rawString,
+ ref,
+} from './virtual_elements';
+export { getData as getNodeData } from './node_data';
+export { unmountComponent } from './util';
diff --git a/packages/melody-idom/src/node_data.ts b/packages/melody-idom/src/node_data.ts
new file mode 100644
index 0000000..6585678
--- /dev/null
+++ b/packages/melody-idom/src/node_data.ts
@@ -0,0 +1,185 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createMap } from './util';
+
+/**
+ * Keeps track of information needed to perform diffs for a given DOM node.
+ * @param {!string} nodeName
+ * @param {?string=} key
+ * @constructor
+ */
+function NodeData(nodeName, key) {
+ /**
+ * The attributes and their values.
+ * @const {!Object}
+ */
+ this.attrs = createMap();
+
+ /**
+ * An array of attribute name/value pairs, used for quickly diffing the
+ * incomming attributes to see if the DOM node's attributes need to be
+ * updated.
+ * @const {Array<*>}
+ */
+ this.attrsArr = [];
+
+ /**
+ * The incoming attributes for this Node, before they are updated.
+ * @const {!Object}
+ */
+ this.newAttrs = createMap();
+
+ /**
+ * The key used to identify this node, used to preserve DOM nodes when they
+ * move within their parent.
+ * @const
+ */
+ this.key = key;
+
+ /**
+ * Keeps track of children within this node by their key.
+ * {?Object}
+ */
+ this.keyMap = createMap();
+
+ /**
+ * Whether or not the keyMap is currently valid.
+ * {boolean}
+ */
+ this.keyMapValid = true;
+
+ /**
+ * Whether or not the statics for the given node have already been applied.
+ *
+ * @type {boolean}
+ */
+ this.staticsApplied = false;
+
+ /**
+ * Whether or not the associated node is or contains a focused Element.
+ * @type {boolean}
+ */
+ this.focused = false;
+
+ /**
+ * The node name for this node.
+ * @const {string}
+ */
+ this.nodeName = nodeName;
+
+ /**
+ * @type {?string}
+ */
+ this.text = null;
+
+ /**
+ * The component instance associated with this element.
+ * @type {Object}
+ */
+ this.componentInstance = null;
+
+ /**
+ * The length of the children in this element.
+ * This value is only calculated for raw elements.
+ * @type {number}
+ */
+ this.childLength = 0;
+}
+
+/**
+ * Initializes a NodeData object for a Node.
+ *
+ * @param {Node} node The node to initialize data for.
+ * @param {string} nodeName The node name of node.
+ * @param {?string=} key The key that identifies the node.
+ * @return {!NodeData} The newly initialized data object
+ */
+var initData = function(node, nodeName, key) {
+ var data = new NodeData(nodeName, key);
+ node['__incrementalDOMData'] = data;
+ return data;
+};
+
+/**
+ * Retrieves the NodeData object for a Node, creating it if necessary.
+ *
+ * @param {Node} node The node to retrieve the data for.
+ * @return {!NodeData} The NodeData for this Node.
+ */
+var getData = function(node) {
+ if (process.env.NODE_ENV !== 'production') {
+ if (!node) {
+ throw new Error("Can't getData for non-existing node.");
+ }
+ }
+ importNode(node);
+ return node['__incrementalDOMData'];
+};
+
+const importNode = function(node) {
+ const stack = [node];
+ while (stack.length) {
+ const node = stack.pop();
+ if (node['__incrementalDOMData']) {
+ continue;
+ }
+ const isElement = node instanceof Element;
+ const nodeName = isElement ? node.localName : node.nodeName;
+ const key = isElement ? node.getAttribute('key') : null;
+ const data = initData(node, nodeName, key);
+
+ if (key) {
+ const parentData =
+ node.parentNode && node.parentNode['__incrementalDOMData'];
+ if (parentData) {
+ parentData.keyMap[key] = node;
+ }
+ }
+
+ if (isElement) {
+ const attributes = node.attributes;
+ const attrs = data.attrs;
+ const newAttrs = data.newAttrs;
+ const attrsArr = data.attrsArr;
+
+ for (let i = 0; i < attributes.length; i += 1) {
+ const attr = attributes[i];
+ const name = attr.name;
+ const value = attr.value;
+
+ attrs[name] = value;
+ newAttrs[name] = undefined;
+ attrsArr.push(name);
+ attrsArr.push(value);
+ }
+
+ for (
+ let child = node.firstChild;
+ child;
+ child = child.nextSibling
+ ) {
+ stack.push(child);
+ }
+ } else if (node.nodeType === 3) {
+ data.text = node.data;
+ }
+ }
+};
+
+/** */
+export { getData, initData, importNode };
diff --git a/packages/melody-idom/src/nodes.ts b/packages/melody-idom/src/nodes.ts
new file mode 100644
index 0000000..24fbb97
--- /dev/null
+++ b/packages/melody-idom/src/nodes.ts
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { getData, initData } from './node_data';
+import { parseHTML } from './util';
+
+/**
+ * Gets the namespace to create an element (of a given tag) in.
+ * @param {string} tag The tag to get the namespace for.
+ * @param {?Node} parent
+ * @return {?string} The namespace to create the tag in.
+ */
+var getNamespaceForTag = function(tag, parent) {
+ if (tag === 'svg') {
+ return 'http://www.w3.org/2000/svg';
+ }
+
+ if (getData(parent).nodeName === 'foreignObject') {
+ return null;
+ }
+
+ return parent.namespaceURI;
+};
+
+/**
+ * Creates an Element.
+ * @param {Document} doc The document with which to create the Element.
+ * @param {?Node} parent
+ * @param {string} tag The tag for the Element.
+ * @param {?string=} key A key to identify the Element.
+ * @param {?Array<*>=} statics An array of attribute name/value pairs of the
+ * static attributes for the Element.
+ * @return {!Element}
+ */
+var createElement = function(doc, parent, tag, key) {
+ var namespace = getNamespaceForTag(tag, parent);
+ var el;
+
+ if (namespace) {
+ el = doc.createElementNS(namespace, tag);
+ } else {
+ el = doc.createElement(tag);
+ }
+
+ initData(el, tag, key);
+
+ return el;
+};
+
+/**
+ * Creates a Text Node.
+ * @param {Document} doc The document with which to create the Element.
+ * @return {!Text}
+ */
+var createText = function(doc) {
+ var node = doc.createTextNode('');
+ initData(node, '#text', null);
+ return node;
+};
+
+var createRaw = function(doc, html) {
+ var children = parseHTML(html);
+ if (!children.length) {
+ const frag = document.createElement('div');
+ frag.appendChild(doc.createTextNode(''));
+ children = frag.childNodes;
+ }
+ var data = initData(children[0], '#raw', null);
+ data.text = html;
+ data.childLength = children.length;
+ return children;
+};
+
+/** */
+export { createElement, createText, createRaw };
diff --git a/packages/melody-idom/src/options.ts b/packages/melody-idom/src/options.ts
new file mode 100644
index 0000000..a58bc16
--- /dev/null
+++ b/packages/melody-idom/src/options.ts
@@ -0,0 +1,32 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/** Global options
+ * @public
+ * @namespace options {Object}
+ */
+export type Options = {
+ /** Hook invoked after a component is mounted. */
+ afterMount?: (component) => void;
+ /** Hook invoked after the DOM is updated with a component's latest render. */
+ afterUpdate?: (component) => void;
+ /** Hook invoked immediately before a component is unmounted. */
+ beforeUnmount?: (component) => void;
+};
+
+const options: Options = {};
+
+export default options;
diff --git a/packages/melody-idom/src/renderQueue.ts b/packages/melody-idom/src/renderQueue.ts
new file mode 100644
index 0000000..a876410
--- /dev/null
+++ b/packages/melody-idom/src/renderQueue.ts
@@ -0,0 +1,392 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { patchOuter, updateComponent, RenderableComponent } from './core';
+import { getParent } from './hierarchy';
+import { debounce } from 'lodash';
+
+interface Node {
+ component: RenderableComponent;
+ next: Node;
+}
+
+export interface Deadline {
+ didTimeout: boolean;
+ timeRemaining(): number;
+}
+
+interface WhatWGEventListenerArgs {
+ capture?: boolean;
+}
+
+interface WhatWGAddEventListenerArgs extends WhatWGEventListenerArgs {
+ passive?: boolean;
+ once?: boolean;
+}
+
+type WhatWGAddEventListener = (
+ type: string,
+ listener: (event: Event) => void,
+ options?: WhatWGAddEventListenerArgs,
+) => void;
+
+let supportsPassiveListeners = false;
+/* istanbul ignore next */
+(document.createElement('div')
+ .addEventListener as WhatWGAddEventListener)('test', function() {}, {
+ get passive() {
+ supportsPassiveListeners = true;
+ return false;
+ },
+});
+
+const BUSY_FRAME_LENGTH = 3;
+const IDLE_FRAME_LENGTH = 30;
+const MESSAGE_KEY =
+ '__melodyPrioritize_' +
+ Math.random()
+ .toString(36)
+ .slice(2);
+export const mountedComponents = new WeakSet();
+
+// by default we assume that we have to deal with a busy frame
+// we can afford a little more time if we can detect that the
+// browser is currently idle (=not scrolling)
+let idealFrameLength = IDLE_FRAME_LENGTH;
+let scrollListenerAttached = false;
+let prioritizationRequested = false;
+let prioritizationDisabled = false;
+
+const NIL: Node = { component: null, next: null };
+let queue: Node = NIL;
+
+function isEmpty(): boolean {
+ return queue === NIL;
+}
+
+function addToQueue(component: RenderableComponent): void {
+ if (queue !== NIL) {
+ // before we schedule this update, we should check a few things first
+ for (let head = queue; head !== NIL; head = head.next) {
+ // 1: Has this component already been scheduled for an update?
+ if (head.component === component) {
+ // if so: we don't need
+ return;
+ }
+
+ // 2: Is the parent of this component already scheduled for an update?
+ if (getParent(component) === head.component) {
+ // if so: we don't need to do anything
+ return;
+ }
+
+ // 3: Is the component a parent of a node within the queue?
+ if (getParent(head.component) === component) {
+ // if so: replace the child with its parent
+ head.component = component;
+ return;
+ }
+
+ if (head.next === NIL) {
+ // insert the new node at the end of the list
+ // we probably want to adjust that once we know how
+ // to prioritize an update
+ head.next = {
+ component,
+ next: NIL,
+ };
+ break;
+ }
+ }
+ } else {
+ queue = {
+ component,
+ next: NIL,
+ };
+ }
+}
+
+export function drop(component: RenderableComponent): void {
+ if (queue === NIL) {
+ return;
+ }
+ if (queue.component === component) {
+ queue = queue.next;
+ }
+ let prev = queue;
+ for (let head = queue.next; head && head !== NIL; head = head.next) {
+ // is the component (or one of its parents) in the queue the removed component?
+ let comp = head.component;
+ do {
+ if (comp === component) {
+ // if so: drop it
+ prev.next = head.next;
+ head = prev;
+ break;
+ }
+ comp = getParent(comp);
+ } while (comp);
+ prev = head;
+ }
+}
+
+function getPriority(node: Node): number {
+ if (!node.component.el) {
+ return -1;
+ }
+ const windowHeight =
+ window.innerHeight || document.documentElement.clientHeight;
+ const { top, bottom } = node.component.el.getBoundingClientRect();
+
+ // is fully visible
+ if (
+ (0 < top && bottom < windowHeight) ||
+ (top < 0 && windowHeight < bottom)
+ ) {
+ return 0;
+ }
+
+ // bottom of component is visible
+ if (top < 0 && 0 < bottom && bottom < windowHeight) {
+ return 1;
+ }
+
+ // top of component is visible
+ if (0 < top && top < windowHeight) {
+ return 2;
+ }
+
+ // not visible, not new
+ return 3;
+}
+
+function prioritizeQueue(queue: Node): Node {
+ const buckets = new Array(4);
+
+ for (let head = queue; head !== NIL; head = head.next) {
+ const bucketIndex = getPriority(head);
+ if (bucketIndex < 0) {
+ continue;
+ }
+ const clone = { component: head.component, next: NIL };
+ if (!buckets[bucketIndex]) {
+ buckets[bucketIndex] = { first: clone, last: clone };
+ } else {
+ buckets[bucketIndex].last.next = clone;
+ buckets[bucketIndex].last = clone;
+ }
+ }
+
+ return buckets.reduceRight(concatWithKnownLast, NIL);
+}
+
+function concatWithKnownLast(queue: Node, { first, last }): Node {
+ const newList = concat(last, queue);
+ return newList === last ? first : newList;
+}
+
+function concat(queue: Node, nextQueue: Node): Node {
+ if (queue === NIL) {
+ return nextQueue;
+ }
+ let p = queue;
+ while (p.next !== NIL) {
+ if (nextQueue === NIL) {
+ return queue;
+ }
+ if (nextQueue.component === p.component) {
+ nextQueue = nextQueue.next;
+ } else {
+ let prev = nextQueue;
+ for (
+ let head = nextQueue.next;
+ head && head !== NIL;
+ head = head.next
+ ) {
+ if (head.component === p.component) {
+ prev.next = head.next;
+ break;
+ }
+ prev = head;
+ }
+ }
+ p = p.next;
+ }
+ p.next = nextQueue;
+
+ return queue;
+}
+
+function pop(): Node | null {
+ if (isEmpty()) {
+ return null;
+ }
+ const head = queue;
+ queue = queue.next;
+ return head;
+}
+
+let isTicking = false;
+function tick(callback: (deadline: Deadline) => void): void {
+ if (isTicking) {
+ return;
+ }
+ isTicking = true;
+ requestAnimationFrame(() => {
+ const startTime = Date.now();
+ callback({
+ didTimeout: false,
+ timeRemaining() {
+ return Math.max(0, idealFrameLength - (Date.now() - startTime));
+ },
+ });
+ });
+}
+
+function drain(): Array {
+ let next = pop();
+ const mounted = [];
+ while (next) {
+ if (next.component.el) {
+ patchOuter(
+ next.component.el,
+ _ => updateComponent(next.component),
+ {},
+ );
+ mounted.push(next.component);
+ }
+ next = pop();
+ }
+ return mounted;
+}
+
+export function flush(deadline: Deadline): void {
+ let prevQueue;
+ let next = pop();
+ let hasNew = false;
+ const mounted = new Set();
+ while (next) {
+ prevQueue = queue;
+ queue = NIL;
+
+ if (next.component.el) {
+ const isNew = next.component.el.localName === 'm-placeholder';
+ patchOuter(
+ next.component.el,
+ _ => updateComponent(next.component),
+ {},
+ );
+ mounted.add(next.component);
+ if (isNew && queue !== NIL) {
+ const drained = drain();
+ for (let i = 0; i < drained.length; i++) {
+ mounted.add(drained[i]);
+ }
+ queue = NIL;
+ }
+ }
+
+ if (queue !== NIL) {
+ hasNew = true;
+ }
+ queue = concat(queue, prevQueue);
+ next = 0 < deadline.timeRemaining() ? pop() : null;
+ }
+ // notify the freshly mounted components
+ const notified = mounted.values();
+ for (
+ let current = notified.next();
+ !current.done;
+ current = notified.next()
+ ) {
+ const comp = current.value;
+ if (comp.el) {
+ mountedComponents.add(comp);
+ comp.notify();
+ }
+ }
+ isTicking = false;
+ if (!isEmpty()) {
+ if (!prioritizationDisabled && !prioritizationRequested && hasNew) {
+ prioritizationRequested = true;
+ window.postMessage(MESSAGE_KEY, '*');
+ }
+ tick(flush);
+ }
+}
+
+export function clear() {
+ if (process.env.NODE_ENV !== 'test') {
+ throw new Error(
+ 'Clearing the queue is only allowed within a test environment.',
+ );
+ }
+ queue = NIL;
+}
+
+function performReordering(event: MessageEvent): void {
+ if (event.source !== this || event.data !== MESSAGE_KEY) {
+ return;
+ }
+ prioritizationRequested = false;
+
+ let timeSpent = Date.now();
+ queue = prioritizeQueue(queue);
+ timeSpent = Date.now() - timeSpent;
+
+ // Usually prioritization takes 0 - 4 ms on fast browsers. If browser is not
+ // able to do that (like Edge/IE) in this period skip the process.
+ if (timeSpent > 10) {
+ prioritizationDisabled = true;
+ }
+}
+
+window.addEventListener('message', performReordering, false);
+
+export function enqueueComponent(component: RenderableComponent): void {
+ /* istanbul ignore if */
+ if (supportsPassiveListeners && !scrollListenerAttached) {
+ attachScrollListener();
+ }
+
+ addToQueue(component);
+ /* istanbul ignore else */
+ if (process.env.NODE_ENV === 'test') {
+ return;
+ }
+ tick(flush);
+}
+
+/* istanbul ignore next */
+var detectIdleCallback = debounce(function detectIdleCallback() {
+ idealFrameLength = IDLE_FRAME_LENGTH;
+}, 300);
+
+/* istanbul ignore next */
+function attachScrollListener(): void {
+ scrollListenerAttached = true;
+ // if we can detect when the browser is busy
+ // then we can assume its idle by default
+ idealFrameLength = IDLE_FRAME_LENGTH;
+ (document.addEventListener as WhatWGAddEventListener)(
+ 'scroll',
+ function() {
+ idealFrameLength = BUSY_FRAME_LENGTH;
+ detectIdleCallback();
+ },
+ { passive: true },
+ );
+}
diff --git a/packages/melody-idom/src/util.ts b/packages/melody-idom/src/util.ts
new file mode 100644
index 0000000..07e8d2d
--- /dev/null
+++ b/packages/melody-idom/src/util.ts
@@ -0,0 +1,64 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { drop, mountedComponents } from './renderQueue';
+import { getChildren, unlink } from './hierarchy';
+import options from './options';
+
+/**
+ * A cached reference to the create function.
+ */
+function Blank() {}
+Blank.prototype = Object.create(null);
+
+/**
+ * Creates an map object without a prototype.
+ * @return {!Object}
+ */
+var createMap = function(): any {
+ return new Blank();
+};
+
+var unmountComponent = function(comp) {
+ getChildren(comp).forEach(unmountComponent);
+ unlink(comp);
+ drop(comp);
+ const data = comp.el ? comp.el['__incrementalDOMData'] : null;
+ if (options.beforeUnmount) {
+ options.beforeUnmount(comp);
+ }
+ if (mountedComponents.has(comp)) {
+ comp.componentWillUnmount();
+ mountedComponents.delete(comp);
+ }
+ if (data && data.componentInstance) {
+ data.componentInstance = null;
+ }
+ comp.el = null;
+};
+
+var documentRange = null;
+
+function parseHTML(htmlString: string): NodeList {
+ if (!documentRange) {
+ documentRange = document.createRange();
+ documentRange.selectNode(document.body);
+ }
+ return documentRange.createContextualFragment(htmlString.trim()).childNodes;
+}
+
+/** */
+export { createMap, parseHTML, unmountComponent };
diff --git a/packages/melody-idom/src/virtual_elements.ts b/packages/melody-idom/src/virtual_elements.ts
new file mode 100644
index 0000000..767948b
--- /dev/null
+++ b/packages/melody-idom/src/virtual_elements.ts
@@ -0,0 +1,360 @@
+/**
+ * Copyright 2015 The Incremental DOM Authors.
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ elementOpen as coreElementOpen,
+ elementClose as coreElementClose,
+ text as coreText,
+ raw as coreRaw,
+ currentComponent,
+} from './core';
+import { updateAttribute } from './attributes';
+import { getData } from './node_data';
+import {
+ assertNotInAttributes,
+ assertNotInSkip,
+ assertInAttributes,
+ assertCloseMatchesOpenTag,
+ setInAttributes,
+} from './assertions';
+
+/**
+ * The offset in the virtual element declaration where the attributes are
+ * specified.
+ * @const
+ */
+var ATTRIBUTES_OFFSET = 3;
+
+/**
+ * Builds an array of arguments for use with elementOpenStart, attr and
+ * elementOpenEnd.
+ * @const {Array<*>}
+ */
+var argsBuilder = [];
+
+/**
+ * @param {string} tag The element's tag.
+ * @param {?string=} key The key used to identify this element. This can be an
+ * empty string, but performance may be better if a unique value is used
+ * when iterating over an array of items.
+ * @param {?Array<*>=} statics An array of attribute name/value pairs of the
+ * static attributes for the Element. These will only be set once when the
+ * Element is created.
+ * @param {...*} var_args Attribute name/value pairs of the dynamic attributes
+ * for the Element.
+ * @return {!Element} The corresponding Element.
+ */
+var elementOpen = function(tag, key, statics, var_args) {
+ if (process.env.NODE_ENV !== 'production') {
+ assertNotInAttributes('elementOpen');
+ assertNotInSkip('elementOpen');
+ }
+
+ var node = coreElementOpen(tag, key);
+ var data = getData(node);
+
+ /*
+ * Checks to see if one or more attributes have changed for a given Element.
+ * When no attributes have changed, this is much faster than checking each
+ * individual argument. When attributes have changed, the overhead of this is
+ * minimal.
+ */
+ const attrsArr = data.attrsArr;
+ const newAttrs = data.newAttrs;
+ const isNew = !attrsArr.length;
+ var i = ATTRIBUTES_OFFSET;
+ var j = 0;
+
+ if (!data.staticsApplied) {
+ if (statics) {
+ for (let i = 0; i < statics.length; i += 2) {
+ const name = statics[i];
+ const value = statics[i + 1];
+ if (newAttrs[name] === undefined) {
+ delete newAttrs[name];
+ }
+ updateAttribute(node, name, value);
+ }
+ }
+ data.staticsApplied = true;
+ }
+
+ for (; i < arguments.length; i += 2, j += 2) {
+ const attr = arguments[i];
+ if (isNew) {
+ attrsArr[j] = attr;
+ newAttrs[attr] = undefined;
+ } else if (attrsArr[j] !== attr) {
+ break;
+ }
+
+ const value = arguments[i + 1];
+ if (isNew || attrsArr[j + 1] !== value) {
+ attrsArr[j + 1] = value;
+ updateAttribute(node, attr, value);
+ }
+ }
+
+ if (i < arguments.length || j < attrsArr.length) {
+ for (; i < arguments.length; i += 1, j += 1) {
+ attrsArr[j] = arguments[i];
+ }
+
+ if (j < attrsArr.length) {
+ attrsArr.length = j;
+ }
+
+ /**
+ * Actually perform the attribute update.
+ */
+ for (i = 0; i < attrsArr.length; i += 2) {
+ newAttrs[attrsArr[i]] = attrsArr[i + 1];
+ }
+
+ for (const attr in newAttrs) {
+ updateAttribute(node, attr, newAttrs[attr]);
+ newAttrs[attr] = undefined;
+ }
+ }
+
+ return node;
+};
+
+/**
+ * Declares a virtual Element at the current location in the document. This
+ * corresponds to an opening tag and a elementClose tag is required. This is
+ * like elementOpen, but the attributes are defined using the attr function
+ * rather than being passed as arguments. Must be folllowed by 0 or more calls
+ * to attr, then a call to elementOpenEnd.
+ * @param {string} tag The element's tag.
+ * @param {?string=} key The key used to identify this element. This can be an
+ * empty string, but performance may be better if a unique value is used
+ * when iterating over an array of items.
+ * @param {?Array<*>=} statics An array of attribute name/value pairs of the
+ * static attributes for the Element. These will only be set once when the
+ * Element is created.
+ */
+var elementOpenStart = function(tag, key, statics, var_args) {
+ if (process.env.NODE_ENV !== 'production') {
+ assertNotInAttributes('elementOpenStart');
+ setInAttributes(true);
+ }
+
+ argsBuilder[0] = tag;
+ argsBuilder[1] = key;
+ argsBuilder[2] = statics;
+
+ var i = ATTRIBUTES_OFFSET;
+ for (; i < arguments.length; i++) {
+ argsBuilder[i] = arguments[i];
+ }
+};
+
+/***
+ * Defines a virtual attribute at this point of the DOM. This is only valid
+ * when called between elementOpenStart and elementOpenEnd.
+ *
+ * @param {string} name
+ * @param {*} value
+ */
+var attr = function(name, value) {
+ if (process.env.NODE_ENV !== 'production') {
+ assertInAttributes('attr');
+ }
+
+ argsBuilder.push(name, value);
+};
+
+/**
+ * Closes an open tag started with elementOpenStart.
+ * @return {!Element} The corresponding Element.
+ */
+var elementOpenEnd = function() {
+ if (process.env.NODE_ENV !== 'production') {
+ assertInAttributes('elementOpenEnd');
+ setInAttributes(false);
+ }
+
+ var node = elementOpen.apply(null, argsBuilder);
+ argsBuilder.length = 0;
+ return node;
+};
+
+/**
+ * Closes an open virtual Element.
+ *
+ * @param {string} tag The element's tag.
+ * @return {!Element} The corresponding Element.
+ */
+var elementClose = function(tag) {
+ if (process.env.NODE_ENV !== 'production') {
+ assertNotInAttributes('elementClose');
+ }
+
+ var node = coreElementClose();
+
+ if (process.env.NODE_ENV !== 'production') {
+ assertCloseMatchesOpenTag(getData(node).nodeName, tag);
+ }
+
+ return node;
+};
+
+/**
+ * Declares a virtual Element at the current location in the document that has
+ * no children.
+ * @param {string} tag The element's tag.
+ * @param {?string=} key The key used to identify this element. This can be an
+ * empty string, but performance may be better if a unique value is used
+ * when iterating over an array of items.
+ * @param {?Array<*>=} statics An array of attribute name/value pairs of the
+ * static attributes for the Element. These will only be set once when the
+ * Element is created.
+ * @param {...*} var_args Attribute name/value pairs of the dynamic attributes
+ * for the Element.
+ * @return {!Element} The corresponding Element.
+ */
+var elementVoid = function(tag, key, statics, var_args) {
+ elementOpen.apply(null, arguments);
+ return elementClose(tag);
+};
+
+var ref = id => element => {
+ let comp = currentComponent();
+ if (process.env.NODE_ENV !== 'production') {
+ if (!comp || !comp.refs) {
+ throw new Error('ref() must be used within a component');
+ }
+ }
+ comp.refs[id] = element;
+ return {
+ unsubscribe() {
+ if (!comp) {
+ return;
+ }
+ comp = null;
+ },
+ };
+};
+
+/**
+ * Creates a new RawString that may contain HTML that should be rendered
+ * as is and should not be escaped.
+ *
+ * @param {string} value The wrapped String.
+ * @class
+ */
+var RawString = function(value) {
+ this.value = value;
+};
+
+/**
+ * Return the wrapped value of the raw string.
+ */
+RawString.prototype.toString = function() {
+ return this.value;
+};
+
+/**
+ * Creates a new RawString that may contain HTML that should be rendered
+ * as is and should not be escaped.
+ *
+ * @param {string} value The wrapped String.
+ */
+var rawString = function(value) {
+ if (value instanceof RawString) {
+ return value;
+ }
+ if (process.env.NODE_ENV !== 'production') {
+ if (typeof value !== 'string') {
+ throw new Error(
+ 'Tried to create a RawString from non-string value: ' +
+ JSON.stringify(value),
+ );
+ }
+ }
+ return new RawString(value);
+};
+
+/**
+ * Declares a virtual Text at this point in the document.
+ *
+ * @param {string|number|boolean|RawString} value The value of the Text.
+ * @param {...(function((string|number|boolean)):string)} var_args
+ * Functions to format the value which are called only when the value has
+ * changed.
+ * @return {!Text} The corresponding text node.
+ */
+var text = function(value, var_args) {
+ if (process.env.NODE_ENV !== 'production') {
+ assertNotInAttributes('text');
+ assertNotInSkip('text');
+ }
+
+ if (value instanceof RawString) {
+ if (process.env.NODE_ENV !== 'production') {
+ if (arguments.length > 1) {
+ throw new Error("Can't call filters on a raw string.");
+ }
+ }
+ return raw(value.value);
+ }
+
+ var node = coreText();
+ var data = getData(node);
+
+ if (data.text !== value) {
+ data.text /** @type {string} */ = value;
+
+ var formatted = value;
+ for (var i = 1; i < arguments.length; i += 1) {
+ /*
+ * Call the formatter function directly to prevent leaking arguments.
+ * https://github.com/google/incremental-dom/pull/204#issuecomment-178223574
+ */
+ var fn = arguments[i];
+ formatted = fn(formatted);
+ }
+
+ node.data = formatted;
+ }
+
+ return node;
+};
+
+var raw = function(value) {
+ if (process.env.NODE_ENV !== 'production') {
+ assertNotInAttributes('text');
+ assertNotInSkip('text');
+ }
+
+ return coreRaw(value);
+};
+
+/** */
+export {
+ elementOpenStart,
+ elementOpenEnd,
+ elementOpen,
+ elementVoid,
+ elementClose,
+ text,
+ attr,
+ ref,
+ raw,
+ rawString,
+};
diff --git a/packages/melody-idom/tsconfig.json b/packages/melody-idom/tsconfig.json
new file mode 100644
index 0000000..e158626
--- /dev/null
+++ b/packages/melody-idom/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./built"
+ },
+ "include": [
+ "./src/**/*"
+ ]
+}
\ No newline at end of file
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/Item.template b/packages/melody-jest-transform/__tests__/__fixtures__/Item.template
new file mode 100644
index 0000000..5d2fd3c
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/Item.template
@@ -0,0 +1,4 @@
+import { createComponent } from 'melody-component';
+import template from './test.twig';
+
+export default createComponent(template);
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/bar.twig b/packages/melody-jest-transform/__tests__/__fixtures__/bar.twig
new file mode 100644
index 0000000..c03132c
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/bar.twig
@@ -0,0 +1,3 @@
+
+ bar
+
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/block.twig b/packages/melody-jest-transform/__tests__/__fixtures__/block.twig
new file mode 100644
index 0000000..77c5a75
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/block.twig
@@ -0,0 +1,14 @@
+{% use "./foo.twig" %}
+
+{% block hello %}
+
+ {{ message | lower | upper }}{% flush %}
+ {{ _context.name[1:] }}
+ {{ block('test') }}
+ {{ include('./test.twig') }}
+ {% include './test.twig' %}
+
+{% endblock %}
+{% block bar foo %}
+{{ block('hello') }}
+
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/embed_nested.twig b/packages/melody-jest-transform/__tests__/__fixtures__/embed_nested.twig
new file mode 100644
index 0000000..852b078
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/embed_nested.twig
@@ -0,0 +1,18 @@
+{% extends "./parent.twig" %}
+
+{% block hello %}
+
+ {% embed "./foo.twig" with { foo: 'bar' } %}
+ {% block hello %}
+ {{ fun }}
+ {% embed "./bar.twig" %}
+ {% block hello %}
+ {{ message }}
+ {% endblock %}
+ {% block test %}
+ {% endblock %}
+ {% endembed %}
+ {% endblock hello %}
+ {% endembed %}
+
+{% endblock %}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/expressions.twig b/packages/melody-jest-transform/__tests__/__fixtures__/expressions.twig
new file mode 100644
index 0000000..87640ad
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/expressions.twig
@@ -0,0 +1,55 @@
+{{ a b-and b }}
+{{ a b-or b }}
+{{ a b-xor b }}
+{{ a or b }}
+{{ a and b }}
+{{ a == b }}
+{{ a != b }}
+{{ a < b }}
+{{ a > b }}
+{{ a >= b }}
+{{ a <= b }}
+{{ a in b }}
+{{ a not in b }}
+{{ a matches b }}
+{{ a matches '^foo' }}
+{{ a starts with b }}
+{{ a ends with b }}
+{{ a..b }}
+{{ a+b }}
+{{ a-b }}
+{{ a~b }}
+{{ a*b }}
+{{ a/b }}
+{{ a%b }}
+{{ a ** b }}
+{{ a ? b }}
+{{ a ?: b }}
+{{ a ?? b }}
+
+{{ a is divisible by(b) }}
+{{ a is not divisible by(b) }}
+{{ a is defined }}
+{{ a is not defined }}
+{{ isEmpty is empty }}
+{{ a is not empty }}
+{{ a is even }}
+{{ a is not even }}
+{{ a is iterable }}
+{{ a is not iterable }}
+{{ a is null }}
+{{ a is not null }}
+{{ a is odd }}
+{{ a is not odd }}
+{{ a is same as(b) }}
+{{ a is not same as(b) }}
+
+{{ dump(test) }}
+{{ range(2, 3) | sort | join(',') }}
+{{ range(3) | sort | join(',') }}
+{{ range(2, 3, 2) | sort | join(',') }}
+{{ test | raw }}
+{{ 2.4 | abs }}
+{{ { a: 'b' } | json_encode | trim }}
+{{ [2, 3] | length }}
+{{ 'test.foo' | split('.') }}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/extends.twig b/packages/melody-jest-transform/__tests__/__fixtures__/extends.twig
new file mode 100644
index 0000000..d22c68e
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/extends.twig
@@ -0,0 +1,11 @@
+{% extends "./parent.twig" %}
+
+{% block hello %}
+
+ {{ message }}&
+ {% include "./test.twig" with { foo: "bar" } only %}
+ {{ include("./test.twig", { foo: "bar" }) }}
+
+ {% mount Item with item %}
+
+{% endblock %}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/extends_with_context.twig b/packages/melody-jest-transform/__tests__/__fixtures__/extends_with_context.twig
new file mode 100644
index 0000000..c77b2e3
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/extends_with_context.twig
@@ -0,0 +1,8 @@
+{% set foo = "bar" %}
+{% extends "./parent.twig" %}
+
+{% block hello %}
+
+ {{ message }}
+
+{% endblock %}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/foo.twig b/packages/melody-jest-transform/__tests__/__fixtures__/foo.twig
new file mode 100644
index 0000000..273e0fb
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/foo.twig
@@ -0,0 +1 @@
+{{ "foo" }}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/for_if_else.twig b/packages/melody-jest-transform/__tests__/__fixtures__/for_if_else.twig
new file mode 100644
index 0000000..b1f04e7
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/for_if_else.twig
@@ -0,0 +1,17 @@
+
+{% for a,b in c | slice(3, c.length) if b is even %}
+ {{ a }} - {{ b }}
+{% else %}
+ No results found
+{% endfor %}
+
+
+
+ {% for a,b in c[:c.length - 1] if b is defined and not b is even %}
+ {{ a }} - {{ b }}
+ {% else %}
+ {% if regionName is empty %}
+ No results found
+ {% endif %}
+ {% endfor %}
+
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/for_local.twig b/packages/melody-jest-transform/__tests__/__fixtures__/for_local.twig
new file mode 100644
index 0000000..eb82cc5
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/for_local.twig
@@ -0,0 +1,7 @@
+
+ {% for item in items %}
+
+ {{ loop.index0 // 2 }} {{ item.name }} {{ loop.index }}
+
+ {% endfor %}
+
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/for_with_block.twig b/packages/melody-jest-transform/__tests__/__fixtures__/for_with_block.twig
new file mode 100644
index 0000000..5b81183
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/for_with_block.twig
@@ -0,0 +1,10 @@
+{{ title | title }}
+
+ {% for item in items %}
+
+ {% block title %}
+ {{ loop.index0 }} {{ item.name | title }} {{ loop.index }}
+ {% endblock %}
+
+ {% endfor %}
+
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/for_with_block_and_key.twig b/packages/melody-jest-transform/__tests__/__fixtures__/for_with_block_and_key.twig
new file mode 100644
index 0000000..468ff47
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/for_with_block_and_key.twig
@@ -0,0 +1,9 @@
+
+ {% for i, item in items %}
+
+ {% block itemContent %}
+ {{i}} {{ loop.index0 }} {{ item.name }} {{ loop.index }}
+ {% endblock %}
+
+ {% endfor %}
+
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/for_with_include.twig b/packages/melody-jest-transform/__tests__/__fixtures__/for_with_include.twig
new file mode 100644
index 0000000..7c4518d
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/for_with_include.twig
@@ -0,0 +1,5 @@
+{% for foo in range(1, category) %}
+
+ {% include './test.twig' %}
+
+{% endfor %}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/itemElement.twig b/packages/melody-jest-transform/__tests__/__fixtures__/itemElement.twig
new file mode 100644
index 0000000..b934761
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/itemElement.twig
@@ -0,0 +1,465 @@
+
+ Deals
+
+
+
+
+
+ No image for this deal.
+
+
+
+ Add the Lorem Ipsum dolor sit amet to my favorites list
+
+
+
+
+
+
+
+ Lorem Ipsum dolor sit amet
+
+
+
Next to location:
+
+
+ Amsterdam,
+ 1.2km to City centre
+
+
+
+
+
+
“Good”
+
+
+
+ 76 / 100 -
+ hotel rating (754 reviews)
+
+
+
+
+
+
+
+
+ Other deals
+
+ View more deals: 17
+
+
+
+
+
Best of 17 websites
+
+
+
+
+
+
+
Change date to see available deals.
+
+
+
+
+
+
+
+
+
+
+
+ Detailed information for this deal
+
+
+ Navigation for detailed information
+
+ Close
+
+
+
+
+
Slideout contents here
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
+
+
+
+
+
+
+
+ No image for this deal.
+
+
+
+ Add the Lorem Ipsum dolor sit amet to my favorites list
+
+
+
+
+
+
+
+ Lorem Ipsum dolor sit amet
+
+
+
Next to location:
+
+
+ Amsterdam,
+ 0.9km to London Heathrow Airport
+
+
+
+
+
+
“Good”
+
+
+
+ 76 / 100 -
+ hotel rating (754 reviews)
+
+
+
+
+
+
+
+
+ Other deals
+
+ View more deals: 17
+
+
+
+
+
Best of 17 websites
+
+
+
+
+
+
+
Change date to see available deals.
+
+
+
+
+
+
+
+
+
+
+
+ Detailed information for this deal
+
+
+ Navigation for detailed information
+
+ Close
+
+
+
+
+
Slideout contents here
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
+
+
+
+
+
+
+
+ No image for this deal.
+
+
+
+ Add the Lorem Ipsum dolor sit amet to my favorites list
+
+
+
+
+
+
+
+ Lorem Ipsum dolor sit amet
+
+
+
Next to location:
+
+
+ Amsterdam,
+ 1.5km to International Airport Amsterdam
+
+
+
+
+
+
“Good”
+
+
+
+ 76 / 100 -
+ hotel rating (754 reviews)
+
+
+
+
+
+
+
+
+ Other deals
+
+ View more deals: 17
+
+
+
+
+
Best of 17 websites
+
+
+
Breakfast included
+
+
+
+
Change date to see available deals.
+
+
+
+
+
+
+
+
+
+
+
+ Detailed information for this deal
+
+
+ Navigation for detailed information
+
+ Close
+
+
+
+
+
Slideout contents here
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
+
+
+
+
+
+
+
+ No image for this deal.
+
+
+
+ Add the Lorem Ipsum dolor sit amet to my favorites list
+
+
+
+
+
+
+
+ Lorem Ipsum dolor sit amet
+
+
+
Next to location:
+
+
+ Amsterdam,
+ 0.5km to Oxford Street
+
+
+
+
+
+
“Good”
+
+
+
+ 76 / 100 -
+ hotel rating (754 reviews)
+
+
+
+
+
+
+
+
+ Other deals
+
+ View more deals: 17
+
+
+
+
+
Best of 17 websites
+
+
+
+
+
+
+
Change date to see available deals.
+
+
+
+
+
+
+
+
+
+
+
+ Detailed information for this deal
+
+
+ Navigation for detailed information
+
+ Close
+
+
+
+
+
Slideout contents here
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
+
+
+
+
+
+
+
+ No image for this deal.
+
+
+
+ Add the Lorem Ipsum dolor sit amet to my favorites list
+
+
+
+
+
+
+
+ Lorem Ipsum dolor sit amet
+
+
+
Next to location:
+
+
+ Amsterdam,
+ 0.8km to Alexanderplatz
+
+
+
+
+
+
“Good”
+
+
+
+ 76 / 100 -
+ hotel rating (754 reviews)
+
+
+
+
+
+
+
+
+ Other deals
+
+ View more deals: 17
+
+
+
+
+
Best of 17 websites
+
+
+
+
+
+
+
Change date to see available deals.
+
+
+
+
+
+
+
+
+
+
+
+ Detailed information for this deal
+
+
+ Navigation for detailed information
+
+ Close
+
+
+
+
+
Slideout contents here
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
+
+
+
+
+
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/macros.twig b/packages/melody-jest-transform/__tests__/__fixtures__/macros.twig
new file mode 100644
index 0000000..424825f
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/macros.twig
@@ -0,0 +1,12 @@
+{% macro input(name, value, type, size) %}
+
+{% endmacro %}
+
+{% import _self as forms %}
+
+{{ foo.bar('test') }}
+{{ forms.input('foo', 'bar', 'baz', 42) }}
+
+{% block test %}
+ {% import _self as forms %}
+{% endblock %}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/mount.twig b/packages/melody-jest-transform/__tests__/__fixtures__/mount.twig
new file mode 100644
index 0000000..1e66063
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/mount.twig
@@ -0,0 +1,7 @@
+{% mount './foo.twig' as 'bar' %}
+{% mount './foo.twig' as 'bar' with {foo: 'bar'} %}
+{% mount './foo.twig' with {foo: 'bar'} %}
+{% mount Foo %}
+{% mount Foo as 'bar' %}
+{% mount Foo as 'bar' with {foo: 'bar'} %}
+{% mount Foo with {foo: 'bar'} %}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/multi_include.twig b/packages/melody-jest-transform/__tests__/__fixtures__/multi_include.twig
new file mode 100644
index 0000000..d5732a7
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/multi_include.twig
@@ -0,0 +1,125 @@
+{% set tEB_SAME_WINDOW_CLICKOUT = false %} {# ExpressBooking #}
+{% set ironItem1p1 = true %} {# #}
+{% set bClickoutClientside = false %}
+{% set sBtnModifiers = '' %}
+{% spaceless %}
+
+
+
+
+ {# if 'WEB-28840' is not active #}
+ {% if not ironItem1p1 %}
+ {% if rateCount is defined %}
+
{{ 'ie_bestdeal' }}
+ {% endif %}
+ {% endif %}
+
+ {% if bestPriceDeal %}
+ {% set flagModifiers = "trv-maincolor-03 font-bright cur-pointer--hover" %}
+ {% if ironItem1p1 %}
+ {% set flagModifiers = "font-bright cur-pointer--hover" %}
+ {% endif %}
+ {% if isTopDeal %}
+ {% set dataVariables = {
+ "data-topdeal-percentage": topDealPercentage,
+ "data-topdeal-price": bestPriceDeal.price,
+ "data-topdeal-compare-price": topDealComparePrice,
+ "data-topdeal-criterion": topDealCriterion,
+ "data-topdeal-criterion-id": topDealCriterionId,
+ "data-topdeal-category": category,
+ "data-topdeal-path-name": pathName,
+ "data-topdeal-overall-liking": overallLiking
+ } %}
+ {% set flagModifiers = flagModifiers ~ " top_deals js_top_deals " ~ topDealCriterion %}
+ {% set dataVariables = dataVariables | merge({'data-topdeal-log': '1'}) %}
+ {% include "./foo.twig" with {
+ "styleModifier": flagModifiers,
+ "dataVariables": dataVariables,
+ "text": "ie_topdeal"
+ } only %}
+ {% else %}
+ {% if iSaving >= 20 %}
+ {% include "./bar.twig" with {
+ "styleModifier": flagModifiers,
+ "dataVariables": dataVariables,
+ "text": "-" ~ saving ~ "%"
+ } only %}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+
+
+
+
+ {# New position of advertiser string depending on WEB-29007 #}
+ {% if false %}
+
+ {% if bestPriceDeal.groupId == 80 and bestPriceDeal.useLocalizedHotelWebsiteLogo %}
+ {{ 'book_hotel_website_test' }}
+ {% else %}
+ {{ bestPriceDeal.sName }}
+ {% endif %}
+
+ {% endif %}
+
+ {% if bestPriceDeal %}
+ {% if maxPriceDeal %}
+
+
+
{{ maxPriceDeal.price }}
+ {% else %}
+
+ {% endif %}
+
{{ bestPriceDeal.price }}
+
+ {# Old/regular position of advertiser string without WEB-29007 #}
+ {% if false %}
+
+ {% if oBestPriceDeal.iGroupId == 80 and oBestPriceDeal.bUseLocalizedHotelWebsiteLogo %}
+ {{ 'book_hotel_website_test' }}
+ {% else %}
+ {{ oBestPriceDeal.sName }}
+ {% endif %}
+
+ {% endif %}
+
+ {% if true %}
+ {% set sBtnModifiers = "btn--deal fl-trailing" %}
+ {% endif %}
+ {% if false %}
+ {% set sBtnModifiers = sBtnModifiers ~ " alt" %}
+ {% endif %}
+ {% elseif state == 1 %}
+
{{ 'unavailable_deal' }}
+
+ {% if true %}
+ {% set sBtnModifiers = "btn--icon btn--deal btn--disabled fl-trailing trv-maincolor-04-very-light" %}
+ {% endif %}
+ {% else %}
+
+ {% if true %}
+ {% set sBtnModifiers = "btn--icon btn--deal btn--disabled fl-trailing trv-maincolor-04-very-light" %}
+ {% endif %}
+ {% endif %}
+
+
+
+ {% include "./test.twig" with { "styleModifier": sBtnModifiers, "text": "deals_forward_new" } only %}
+
+
+{% endspaceless %}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/parent.twig b/packages/melody-jest-transform/__tests__/__fixtures__/parent.twig
new file mode 100644
index 0000000..ab68f87
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/parent.twig
@@ -0,0 +1,3 @@
+
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/raw.twig b/packages/melody-jest-transform/__tests__/__fixtures__/raw.twig
new file mode 100644
index 0000000..e5b7ecd
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/raw.twig
@@ -0,0 +1,7 @@
+{% set vars = {
+ "foo": 'foo ',
+ "bar": 'bar ' | raw
+} %}
+
+{{ vars.foo | raw }}
+{{ vars.bar }}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/ref.twig b/packages/melody-jest-transform/__tests__/__fixtures__/ref.twig
new file mode 100644
index 0000000..6a60b0d
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/ref.twig
@@ -0,0 +1,7 @@
+
+ udisuabcd
+
+
+
+ test
+
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/set.twig b/packages/melody-jest-transform/__tests__/__fixtures__/set.twig
new file mode 100644
index 0000000..c1f11fa
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/set.twig
@@ -0,0 +1,9 @@
+{% set list = [1, 2] %}
+{% set foo = 0 %}
+{% for item in list %}
+ {% set foo = item %}
+ {% set bar = item %}
+{% endfor %}
+{% set bar = 0 %}
+{{ foo }}
+{{ bar }}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/spaceless.twig b/packages/melody-jest-transform/__tests__/__fixtures__/spaceless.twig
new file mode 100644
index 0000000..34c6bff
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/spaceless.twig
@@ -0,0 +1,6 @@
+{% spaceless %}
+
+ Receive {{ formattedIncentive }} cash back for testing this hotel.
+ Or just be happy!
+
+{% endspaceless %}
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/svg.twig b/packages/melody-jest-transform/__tests__/__fixtures__/svg.twig
new file mode 100644
index 0000000..a8c7a6a
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/svg.twig
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/melody-jest-transform/__tests__/__fixtures__/test.twig b/packages/melody-jest-transform/__tests__/__fixtures__/test.twig
new file mode 100644
index 0000000..179f993
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__fixtures__/test.twig
@@ -0,0 +1 @@
+test
diff --git a/packages/melody-jest-transform/__tests__/__snapshots__/customTransformSpec.js.snap b/packages/melody-jest-transform/__tests__/__snapshots__/customTransformSpec.js.snap
new file mode 100644
index 0000000..1dd88ea
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/__snapshots__/customTransformSpec.js.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Custom transformer should return compiled melody code when noBabel set 1`] = `
+"import { text, elementOpen, elementClose } from \\"melody-idom\\";
+export const _template = {};
+
+_template.render = function (_context) {
+ elementOpen(\\"div\\", null, null);
+ text(\\"test\\");
+ elementClose(\\"div\\");
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+export default function Test(props) {
+ return _template.render(props);
+}"
+`;
+
+exports[`Custom transformer should throw error if it cannot find a config in regular locations 1`] = `"Couldn't find .babelrc or babel entry on package.json! You can specify custom config with \\"transformer\\". Please consult documentation."`;
+
+exports[`Custom transformer should transpile with jest's process function 1`] = `undefined`;
+
+exports[`Custom transformer should work with default babelconfig and melody-plugins 1`] = `
+"\\"use strict\\";
+
+Object.defineProperty(exports, \\"__esModule\\", {
+ value: true
+});
+exports._template = undefined;
+exports.default = Test;
+
+var _melodyIdom = require(\\"melody-idom\\");
+
+const _template = exports._template = {};
+
+_template.render = function (_context) {
+ (0, _melodyIdom.elementOpen)(\\"div\\", null, null);
+ (0, _melodyIdom.text)(\\"test\\");
+ (0, _melodyIdom.elementClose)(\\"div\\");
+};
+
+if (\\"test\\" !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+function Test(props) {
+ return _template.render(props);
+}"
+`;
diff --git a/packages/melody-jest-transform/__tests__/customTransformSpec.js b/packages/melody-jest-transform/__tests__/customTransformSpec.js
new file mode 100644
index 0000000..8b9c547
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/customTransformSpec.js
@@ -0,0 +1,85 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { extension as CoreExtension } from 'melody-extension-core';
+import idomPlugin from 'melody-plugin-idom';
+import { transformer, process } from '../src';
+import * as fs from 'fs';
+import * as path from 'path';
+
+jest.mock('find-babel-config', () => ({
+ sync: jest.fn().mockImplementation(path => ({
+ config: {
+ env: {
+ test: {
+ presets: ['node6', 'stage-1'],
+ plugins: ['transform-inline-environment-variables'],
+ },
+ },
+ },
+ })),
+}));
+
+describe('Custom transformer', () => {
+ const getFixture = twigPath => {
+ const fixturePrefix = './__fixtures__/';
+ const filePath = path.join(__dirname, fixturePrefix, twigPath);
+
+ return fs.readFileSync(filePath).toString();
+ };
+
+ const test = (fixtureName, options, processFn) => {
+ const fixture = getFixture(fixtureName);
+ const result = processFn
+ ? process(fixture, fixtureName)
+ : transformer(fixture, fixtureName, options);
+ expect(result).toMatchSnapshot();
+ };
+
+ it('should work with default babelconfig and melody-plugins', () => {
+ test('test.twig');
+ });
+
+ it('should work with custom babelConfig and melody-plugins', () => {
+ const plugins = [
+ idomPlugin,
+ {
+ ...CoreExtension,
+ options: {
+ svgFilePath: 'testFilePath',
+ embedSvgAsMelody: true,
+ },
+ },
+ ];
+ });
+
+ it('should throw error if it cannot find a config in regular locations', () => {
+ const mockedFindBabel = require('find-babel-config');
+ mockedFindBabel.sync.mockImplementationOnce(() => false);
+ const regularFixture = getFixture('test.twig');
+ expect(() =>
+ transformer(regularFixture, 'test.twig'),
+ ).toThrowErrorMatchingSnapshot();
+ });
+
+ it('should return compiled melody code when noBabel set', () => {
+ test('test.twig', { noBabel: true });
+ });
+
+ it("should transpile with jest's process function", () => {
+ // eslint-disable-line quotes
+ test('test.twig', {}, true);
+ });
+});
diff --git a/packages/melody-jest-transform/__tests__/transformIntSpec.js b/packages/melody-jest-transform/__tests__/transformIntSpec.js
new file mode 100644
index 0000000..9ae2508
--- /dev/null
+++ b/packages/melody-jest-transform/__tests__/transformIntSpec.js
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './__fixtures__/block.twig';
+import './__fixtures__/embed_nested.twig';
+import './__fixtures__/expressions.twig';
+import './__fixtures__/extends.twig';
+import './__fixtures__/extends_with_context.twig';
+import './__fixtures__/for_if_else.twig';
+import './__fixtures__/for_local.twig';
+import './__fixtures__/for_with_block.twig';
+import './__fixtures__/for_with_block_and_key.twig';
+import './__fixtures__/for_with_include.twig';
+import './__fixtures__/itemElement.twig';
+import './__fixtures__/macros.twig';
+import './__fixtures__/mount.twig';
+import './__fixtures__/multi_include.twig';
+import './__fixtures__/raw.twig';
+import './__fixtures__/ref.twig';
+import './__fixtures__/set.twig';
+import './__fixtures__/spaceless.twig';
+import './__fixtures__/svg.twig';
+
+describe('Transform', () => {
+ it('should successfully transform compiler templates', () => {
+ /*
+ * If this starts it means your twig templates are compiled correctly
+ * by melody-jest-transform since it is preprocessed by jest
+ */
+
+ expect(true).toBe(true);
+ });
+});
diff --git a/packages/melody-jest-transform/package.json b/packages/melody-jest-transform/package.json
new file mode 100644
index 0000000..382ba18
--- /dev/null
+++ b/packages/melody-jest-transform/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "melody-jest-transform",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "babel-core": "^6.18.0",
+ "find-babel-config": "^1.0.1",
+ "melody-compiler": "^0.11.1-rc.1",
+ "melody-extension-core": "^0.11.1-rc.1"
+ },
+ "devDependencies": {
+ "melody-plugin-idom": "^0.11.1-rc.1",
+ "rollup-plugin-babel": "^2.6.1"
+ },
+ "peerDependencies": {
+ "melody-idom": "^0.10.1",
+ "melody-parser": "^0.10.1",
+ "melody-runtime": "^0.10.1",
+ "melody-traverse": "^0.10.1",
+ "melody-types": "^0.10.1"
+ }
+}
diff --git a/packages/melody-jest-transform/src/index.js b/packages/melody-jest-transform/src/index.js
new file mode 100644
index 0000000..a3446c8
--- /dev/null
+++ b/packages/melody-jest-transform/src/index.js
@@ -0,0 +1,16 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { process, transformer } from './preprocessor';
diff --git a/packages/melody-jest-transform/src/preprocessor.js b/packages/melody-jest-transform/src/preprocessor.js
new file mode 100644
index 0000000..fe00b63
--- /dev/null
+++ b/packages/melody-jest-transform/src/preprocessor.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { compile, toString } from 'melody-compiler';
+import * as babel from 'babel-core';
+import { extension as CoreExtension } from 'melody-extension-core';
+import idomPlugin from 'melody-plugin-idom';
+import * as p from 'process';
+import findBabelConfig from 'find-babel-config';
+
+export function getBabelConf() {
+ const { config } = findBabelConfig.sync(p.cwd());
+
+ if (!config) {
+ throw new Error(
+ 'Couldn\'t find .babelrc or babel entry on package.json! You can specify custom config with "transformer". Please consult documentation.',
+ );
+ }
+ return config;
+}
+
+export function process(src, path) {
+ transformer(src, path);
+}
+
+export function transformer(src, path, options = {}) {
+ const plugins = options.plugins || [CoreExtension, idomPlugin];
+ const babelConfig = options.babel || getBabelConf();
+
+ const compiledMelody = toString(compile(path, src, ...plugins), src).code;
+ if (options.noBabel) {
+ return compiledMelody;
+ }
+
+ return babel.transform(compiledMelody, babelConfig).code;
+}
diff --git a/packages/melody-loader/__fixtures__/simple/index.js b/packages/melody-loader/__fixtures__/simple/index.js
new file mode 100644
index 0000000..220373d
--- /dev/null
+++ b/packages/melody-loader/__fixtures__/simple/index.js
@@ -0,0 +1,2 @@
+import template from './template.melody.twig';
+template();
diff --git a/packages/melody-loader/__fixtures__/simple/template.melody.twig b/packages/melody-loader/__fixtures__/simple/template.melody.twig
new file mode 100644
index 0000000..52ea02c
--- /dev/null
+++ b/packages/melody-loader/__fixtures__/simple/template.melody.twig
@@ -0,0 +1 @@
+Hello world
diff --git a/packages/melody-loader/__tests__/__snapshots__/integration.spec.js.snap b/packages/melody-loader/__tests__/__snapshots__/integration.spec.js.snap
new file mode 100644
index 0000000..1dbecbb
--- /dev/null
+++ b/packages/melody-loader/__tests__/__snapshots__/integration.spec.js.snap
@@ -0,0 +1,48 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`melody-loader intergration tests transforms melody template to js 1`] = `
+"webpackJsonp([0],{
+
+/***/ 4:
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+\\"use strict\\";
+Object.defineProperty(__webpack_exports__, \\"__esModule\\", { value: true });
+/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__template_melody_twig__ = __webpack_require__(5);
+
+Object(__WEBPACK_IMPORTED_MODULE_0__template_melody_twig__[\\"a\\" /* default */])();
+
+
+/***/ }),
+
+/***/ 5:
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+\\"use strict\\";
+/* WEBPACK VAR INJECTION */(function(process) {/* harmony export (immutable) */ __webpack_exports__[\\"a\\"] = Template;
+/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_melody_idom__ = __webpack_require__(6);
+/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_melody_idom___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_melody_idom__);
+
+const _template = {};
+/* unused harmony export _template */
+
+
+_template.render = function (_context) {
+ Object(__WEBPACK_IMPORTED_MODULE_0_melody_idom__[\\"elementOpen\\"])(\\"div\\", null, null);
+ Object(__WEBPACK_IMPORTED_MODULE_0_melody_idom__[\\"text\\"])(\\"Hello world\\");
+ Object(__WEBPACK_IMPORTED_MODULE_0_melody_idom__[\\"elementClose\\"])(\\"div\\");
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Template\\";
+}
+
+function Template(props) {
+ return _template.render(props);
+}
+/* WEBPACK VAR INJECTION */}.call(__webpack_exports__, __webpack_require__(0)))
+
+/***/ })
+
+},[4]);"
+`;
diff --git a/packages/melody-loader/__tests__/integration.spec.js b/packages/melody-loader/__tests__/integration.spec.js
new file mode 100644
index 0000000..974e625
--- /dev/null
+++ b/packages/melody-loader/__tests__/integration.spec.js
@@ -0,0 +1,121 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import webpack from 'webpack';
+import { readFile, mkdtemp } from 'mz/fs';
+import { join, resolve } from 'path';
+import rimraf from 'rimraf';
+import mkdirp from 'mkdirp';
+
+// eslint-disable-next-line no-undef
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
+const TMP_DIR = resolve(__dirname, '.tmp');
+
+beforeAll(async () => {
+ await new Promise((res, rej) => {
+ mkdirp(TMP_DIR, err => {
+ return err ? rej(err) : res();
+ });
+ });
+});
+
+afterAll(async () => {
+ await new Promise((res, rej) => {
+ rimraf(TMP_DIR, err => {
+ return err ? rej(err) : res();
+ });
+ });
+});
+
+describe('melody-loader intergration tests', async () => {
+ test('transforms melody template to js', async () => {
+ const compiler = await createCompiler({
+ context: resolve(__dirname, '../__fixtures__/simple'),
+ entry: {
+ main: './index.js',
+ },
+ });
+ const stats = await run(compiler);
+ const asset = stats.compilation.assets['main.js'];
+ const assetContent = await readFile(asset.existsAt, 'utf8');
+ expect(assetContent).toMatchSnapshot();
+ });
+});
+
+async function run(compiler) {
+ return new Promise((res, rej) => {
+ compiler.run((err, stats) => {
+ if (err) {
+ return rej(err);
+ }
+
+ throwOnErrors(stats);
+ res(stats);
+ });
+ });
+}
+
+function throwOnErrors(stats) {
+ const errors = stats.compilation.errors;
+ if (errors.length > 0) {
+ throw new Error(errors[0]);
+ }
+}
+
+async function createConfig({ context, entry }) {
+ const outputDir = await mkdtemp(join(TMP_DIR, '/'));
+
+ return {
+ context,
+ entry,
+ output: {
+ path: outputDir,
+ filename: '[name].js',
+ chunkFilename: '[chunkhash].js',
+ },
+ plugins: [
+ new webpack.optimize.CommonsChunkPlugin({
+ name: 'vendor',
+ minChunks: module => {
+ // put everything into vendor except the fixture
+ // assets
+ return (
+ module.resource && !/__fixtures__/.test(module.resource)
+ );
+ },
+ }),
+ ],
+ module: {
+ rules: [
+ {
+ test: /\.melody\.twig$/,
+ use: [
+ {
+ loader: require.resolve('../src/index'),
+ options: {
+ plugins: ['idom'],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ };
+}
+
+async function createCompiler({ context, entry }) {
+ const config = await createConfig({ context, entry });
+ return webpack(config);
+}
diff --git a/packages/melody-loader/package.json b/packages/melody-loader/package.json
new file mode 100644
index 0000000..5183532
--- /dev/null
+++ b/packages/melody-loader/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "melody-loader",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "loader-utils": "^1.1.0",
+ "lodash": "^4.12.0",
+ "melody-compiler": "^0.11.1-rc.1",
+ "melody-extension-core": "^0.11.1-rc.1",
+ "melody-runtime": "^0.11.1-rc.1"
+ },
+ "peerDependencies": {
+ "melody-idom": "^0.11.1-rc.1"
+ },
+ "devDependencies": {
+ "melody-idom": "^0.11.1-rc.1",
+ "melody-plugin-idom": "^0.11.1-rc.1",
+ "mkdirp": "^0.5.1",
+ "mz": "^2.6.0",
+ "rimraf": "^2.6.1",
+ "rollup-plugin-babel": "^2.6.1",
+ "webpack": "^3.5.5"
+ }
+}
diff --git a/packages/melody-loader/src/index.js b/packages/melody-loader/src/index.js
new file mode 100644
index 0000000..179c2f2
--- /dev/null
+++ b/packages/melody-loader/src/index.js
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { toString, compile } from 'melody-compiler';
+import { extension as CoreExtension } from 'melody-extension-core';
+import { getOptions } from 'loader-utils';
+import { isString, isObject } from 'lodash';
+
+module.exports = function loader(content) {
+ this.cacheable();
+
+ const loaderOptions = getOptions(this) || {
+ plugins: [],
+ };
+
+ const args = [this.resourcePath, content, CoreExtension];
+ if (loaderOptions.plugins) {
+ for (const pluginName of loaderOptions.plugins) {
+ if (isString(pluginName)) {
+ try {
+ args.push(require('melody-plugin-' + pluginName));
+ } catch (e) {
+ this.emitWarning(
+ 'Could not find plugin ' +
+ pluginName +
+ '. Expected name to be melody-plugin-' +
+ pluginName,
+ );
+ }
+ } else if (isObject(pluginName)) {
+ args.push(pluginName);
+ } else {
+ this.emitWarning(
+ 'Value passed as Melody plugin must be string or object. ' +
+ pluginName +
+ ' of type ' +
+ typeof pluginName +
+ ' was given',
+ );
+ }
+ }
+ }
+
+ try {
+ const result = toString(compile.apply(null, args), content);
+ return result.code;
+ } catch (e) {
+ this.emitError(e);
+ return (
+ 'import {text} from "melody-idom"; export default { render(options) { text("Could not load ' +
+ this.resourcePath +
+ '"); console.error("Could not load ' +
+ this.resourcePath +
+ '", ' +
+ JSON.stringify(e.message) +
+ '); } };'
+ );
+ }
+};
diff --git a/packages/melody-parser/package.json b/packages/melody-parser/package.json
new file mode 100644
index 0000000..a3234b2
--- /dev/null
+++ b/packages/melody-parser/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "melody-parser",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "he": "^1.1.0",
+ "lodash": "^4.12.0",
+ "melody-code-frame": "^0.11.1-rc.1"
+ },
+ "bundledDependencies": [
+ "he"
+ ],
+ "peerDependencies": {
+ "melody-types": "^0.10.0"
+ },
+ "devDependencies": {
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-parser/src/Associativity.js b/packages/melody-parser/src/Associativity.js
new file mode 100644
index 0000000..d66f808
--- /dev/null
+++ b/packages/melody-parser/src/Associativity.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export var LEFT = Symbol();
+export var RIGHT = Symbol();
diff --git a/packages/melody-parser/src/CharStream.js b/packages/melody-parser/src/CharStream.js
new file mode 100644
index 0000000..70e5ff1
--- /dev/null
+++ b/packages/melody-parser/src/CharStream.js
@@ -0,0 +1,82 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const EOF = Symbol();
+
+export class CharStream {
+ constructor(input) {
+ this.input = String(input);
+ this.length = this.input.length;
+ this.index = 0;
+ this.position = { line: 1, column: 0 };
+ }
+
+ get source() {
+ return this.input;
+ }
+
+ reset() {
+ this.rewind({ line: 1, column: 0, index: 0 });
+ }
+
+ mark() {
+ let { line, column } = this.position,
+ index = this.index;
+ return { line, column, index };
+ }
+
+ rewind(marker) {
+ this.position.line = marker.line;
+ this.position.column = marker.column;
+ this.index = marker.index;
+ }
+
+ la(offset) {
+ var index = this.index + offset;
+ return index < this.length ? this.input.charAt(index) : EOF;
+ }
+
+ lac(offset) {
+ var index = this.index + offset;
+ return index < this.length ? this.input.charCodeAt(index) : EOF;
+ }
+
+ next() {
+ if (this.index === this.length) {
+ return EOF;
+ }
+ var ch = this.input.charAt(this.index);
+ this.index++;
+ this.position.column++;
+ if (ch === '\n') {
+ this.position.line += 1;
+ this.position.column = 0;
+ }
+ return ch;
+ }
+
+ match(str) {
+ const start = this.mark();
+ for (let i = 0, len = str.length; i < len; i++) {
+ const ch = this.next();
+ if (ch !== str.charAt(i) || ch === EOF) {
+ this.rewind(start);
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/packages/melody-parser/src/Lexer.js b/packages/melody-parser/src/Lexer.js
new file mode 100644
index 0000000..0880cd8
--- /dev/null
+++ b/packages/melody-parser/src/Lexer.js
@@ -0,0 +1,602 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as TokenTypes from './TokenTypes';
+import { EOF } from './CharStream';
+
+const State = {
+ TEXT: 'TEXT',
+ EXPRESSION: 'EXPRESSION',
+ TAG: 'TAG',
+ INTERPOLATION: 'INTERPOLATION',
+ STRING_SINGLE: 'STRING_SINGLE',
+ STRING_DOUBLE: 'STRING_DOUBLE',
+ ELEMENT: 'ELEMENT',
+ ATTRIBUTE_VALUE: 'ATTRIBUTE_VALUE',
+};
+
+const STATE = Symbol(),
+ OPERATORS = Symbol(),
+ STRING_START = Symbol();
+
+const CHAR_TO_TOKEN = {
+ '[': TokenTypes.LBRACE,
+ ']': TokenTypes.RBRACE,
+ '(': TokenTypes.LPAREN,
+ ')': TokenTypes.RPAREN,
+ '{': TokenTypes.LBRACKET,
+ '}': TokenTypes.RBRACKET,
+ ':': TokenTypes.COLON,
+ '.': TokenTypes.DOT,
+ '|': TokenTypes.PIPE,
+ ',': TokenTypes.COMMA,
+ '?': TokenTypes.QUESTION_MARK,
+ '=': TokenTypes.ASSIGNMENT,
+ //'<': TokenTypes.ELEMENT_START,
+ //'>': TokenTypes.ELEMENT_END,
+ '/': TokenTypes.SLASH,
+};
+
+export default class Lexer {
+ constructor(input) {
+ this.input = input;
+ this[STATE] = [State.TEXT];
+ this[OPERATORS] = [];
+ this[STRING_START] = null;
+ }
+
+ reset() {
+ this.input.reset();
+ this[STATE] = [State.TEXT];
+ }
+
+ get source() {
+ return this.input.source;
+ }
+
+ addOperators(...ops) {
+ this[OPERATORS].push(...ops);
+ this[OPERATORS].sort((a, b) => (a.length > b.length ? -1 : 1));
+ }
+
+ get state() {
+ return this[STATE][this[STATE].length - 1];
+ }
+
+ pushState(state) {
+ this[STATE].push(state);
+ }
+
+ popState() {
+ this[STATE].length--;
+ }
+
+ createToken(type, pos) {
+ let input = this.input,
+ endPos = input.mark(),
+ end = endPos.index;
+ return {
+ type,
+ pos,
+ endPos,
+ end,
+ length: end - pos.index,
+ source: input.input,
+ text: input.input.substr(pos.index, end - pos.index),
+ toString: function() {
+ return this.text;
+ },
+ };
+ }
+
+ next() {
+ let input = this.input,
+ pos,
+ c;
+ while ((c = input.la(0)) !== EOF) {
+ pos = input.mark();
+ if (
+ this.state !== State.TEXT &&
+ this.state !== State.STRING_DOUBLE &&
+ this.state !== State.STRING_SINGLE &&
+ this.state !== State.ATTRIBUTE_VALUE &&
+ isWhitespace(c)
+ ) {
+ input.next();
+ while ((c = input.la(0)) !== EOF && isWhitespace(c)) {
+ input.next();
+ }
+ return this.createToken(TokenTypes.WHITESPACE, pos);
+ }
+ if (c === '{' && input.la(1) === '#') {
+ input.next();
+ input.next();
+ if (input.la(0) === '-') {
+ input.next();
+ }
+ while ((c = input.la(0)) !== EOF) {
+ if (
+ (c === '#' && input.la(1) === '}') ||
+ (c === '-' &&
+ input.la(1) === '#' &&
+ input.la(2) === '}')
+ ) {
+ if (c === '-') {
+ input.next();
+ }
+ input.next();
+ input.next();
+ return this.createToken(TokenTypes.COMMENT, pos);
+ }
+ input.next();
+ }
+ }
+ if (this.state === State.TEXT) {
+ let entityToken;
+ if (c === '<') {
+ if (
+ input.la(1) === '{' ||
+ isAlpha(input.lac(1)) ||
+ input.la(1) === '/'
+ ) {
+ input.next();
+ this.pushState(State.ELEMENT);
+ return this.createToken(TokenTypes.ELEMENT_START, pos);
+ } else if (
+ input.la(1) === '!' &&
+ input.la(2) === '-' &&
+ input.la(3) === '-'
+ ) {
+ // match HTML comment
+ input.next(); // <
+ input.next(); // !
+ input.next(); // -
+ input.next(); // -
+ while ((c = input.la(0)) !== EOF) {
+ if (c === '-' && input.la(1) === '-') {
+ input.next();
+ input.next();
+ if (!(c = input.next()) === '>') {
+ this.error(
+ 'Unexpected end for HTML comment',
+ input.mark(),
+ `Expected comment to end with '>' but found '${c}' instead.`,
+ );
+ }
+ break;
+ }
+ input.next();
+ }
+ return this.createToken(TokenTypes.HTML_COMMENT, pos);
+ } else {
+ return this.matchText(pos);
+ }
+ } else if (c === '{') {
+ return this.matchExpressionToken(pos);
+ } else if (c === '&' && (entityToken = this.matchEntity(pos))) {
+ return entityToken;
+ } else {
+ return this.matchText(pos);
+ }
+ } else if (this.state === State.EXPRESSION) {
+ if (
+ (c === '}' && input.la(1) === '}') ||
+ (c === '-' && input.la(1) === '}' && input.la(2) === '}')
+ ) {
+ if (c === '-') {
+ input.next();
+ }
+ input.next();
+ input.next();
+ this.popState();
+ return this.createToken(TokenTypes.EXPRESSION_END, pos);
+ }
+ return this.matchExpression(pos);
+ } else if (this.state === State.TAG) {
+ if (
+ (c === '%' && input.la(1) === '}') ||
+ (c === '-' && input.la(1) === '%' && input.la(2) === '}')
+ ) {
+ if (c === '-') {
+ input.next();
+ }
+ input.next();
+ input.next();
+ this.popState();
+ return this.createToken(TokenTypes.TAG_END, pos);
+ }
+ return this.matchExpression(pos);
+ } else if (
+ this.state === State.STRING_SINGLE ||
+ this.state === State.STRING_DOUBLE
+ ) {
+ return this.matchString(pos, true);
+ } else if (this.state === State.INTERPOLATION) {
+ if (c === '}') {
+ input.next();
+ this.popState(); // pop interpolation
+ return this.createToken(TokenTypes.INTERPOLATION_END, pos);
+ }
+ return this.matchExpression(pos);
+ } else if (this.state === State.ELEMENT) {
+ switch (c) {
+ case '/':
+ input.next();
+ return this.createToken(TokenTypes.SLASH, pos);
+ case '{':
+ return this.matchExpressionToken(pos);
+ case '>':
+ input.next();
+ this.popState();
+ return this.createToken(TokenTypes.ELEMENT_END, pos);
+ case '"':
+ input.next();
+ this.pushState(State.ATTRIBUTE_VALUE);
+ return this.createToken(TokenTypes.STRING_START, pos);
+ case '=':
+ input.next();
+ return this.createToken(TokenTypes.ASSIGNMENT, pos);
+ default:
+ return this.matchSymbol(pos);
+ }
+ } else if (this.state === State.ATTRIBUTE_VALUE) {
+ if (c === '"') {
+ input.next();
+ this.popState();
+ return this.createToken(TokenTypes.STRING_END, pos);
+ } else {
+ return this.matchAttributeValue(pos);
+ }
+ } else {
+ return this.error(`Invalid state ${this.state}`, pos);
+ }
+ }
+ return TokenTypes.EOF_TOKEN;
+ }
+
+ matchExpressionToken(pos) {
+ const input = this.input;
+ switch (input.la(1)) {
+ case '{':
+ input.next();
+ input.next();
+ this.pushState(State.EXPRESSION);
+ if (input.la(0) === '-') {
+ input.next();
+ }
+ return this.createToken(TokenTypes.EXPRESSION_START, pos);
+ case '%':
+ input.next();
+ input.next();
+ this.pushState(State.TAG);
+ if (input.la(0) === '-') {
+ input.next();
+ }
+ return this.createToken(TokenTypes.TAG_START, pos);
+ case '#':
+ input.next();
+ input.next();
+ if (input.la(0) === '-') {
+ input.next();
+ }
+ return this.matchComment(pos);
+ default:
+ return this.matchText(pos);
+ }
+ }
+
+ matchExpression(pos) {
+ let input = this.input,
+ c = input.la(0);
+ switch (c) {
+ case "'":
+ this.pushState(State.STRING_SINGLE);
+ input.next();
+ return this.createToken(TokenTypes.STRING_START, pos);
+ case '"':
+ this.pushState(State.STRING_DOUBLE);
+ input.next();
+ return this.createToken(TokenTypes.STRING_START, pos);
+ default: {
+ if (isDigit(input.lac(0))) {
+ input.next();
+ return this.matchNumber(pos);
+ }
+ if (
+ (c === 't' && input.match('true')) ||
+ (c === 'T' && input.match('TRUE'))
+ ) {
+ return this.createToken(TokenTypes.TRUE, pos);
+ }
+ if (
+ (c === 'f' && input.match('false')) ||
+ (c === 'F' && input.match('FALSE'))
+ ) {
+ return this.createToken(TokenTypes.FALSE, pos);
+ }
+ if (
+ (c === 'n' &&
+ (input.match('null') || input.match('none'))) ||
+ (c === 'N' && (input.match('NULL') || input.match('NONE')))
+ ) {
+ return this.createToken(TokenTypes.NULL, pos);
+ }
+ const {
+ longestMatchingOperator,
+ longestMatchEndPos,
+ } = this.findLongestMatchingOperator();
+ const cc = input.lac(0);
+ if (cc === 95 /* _ */ || isAlpha(cc) || isDigit(cc)) {
+ // okay... this could be either a symbol or an operator
+ input.next();
+ const sym = this.matchSymbol(pos);
+ if (sym.text.length <= longestMatchingOperator.length) {
+ // the operator was longer so let's use that
+ input.rewind(longestMatchEndPos);
+ return this.createToken(TokenTypes.OPERATOR, pos);
+ }
+ // found a symbol
+ return sym;
+ } else if (longestMatchingOperator) {
+ input.rewind(longestMatchEndPos);
+ return this.createToken(TokenTypes.OPERATOR, pos);
+ } else if (CHAR_TO_TOKEN.hasOwnProperty(c)) {
+ input.next();
+ return this.createToken(CHAR_TO_TOKEN[c], pos);
+ } else {
+ return this.error(`Unknown token ${c}`, pos);
+ }
+ }
+ }
+ }
+
+ findLongestMatchingOperator() {
+ const input = this.input,
+ start = input.mark();
+ let longestMatchingOperator = '',
+ longestMatchEndPos = null;
+ for (let i = 0, ops = this[OPERATORS], len = ops.length; i < len; i++) {
+ const op = ops[i];
+ if (op.length > longestMatchingOperator.length && input.match(op)) {
+ longestMatchingOperator = op;
+ longestMatchEndPos = input.mark();
+ input.rewind(start);
+ }
+ }
+ input.rewind(start);
+ return { longestMatchingOperator, longestMatchEndPos };
+ }
+
+ error(message, pos, advice = '') {
+ const errorToken = this.createToken(TokenTypes.ERROR, pos);
+ errorToken.message = message;
+ errorToken.advice = advice;
+ return errorToken;
+ }
+
+ matchEntity(pos) {
+ const input = this.input;
+ input.next(); // &
+ if (input.la(0) === '#') {
+ input.next(); // #
+ if (input.la(0) === 'x') {
+ // hexadecimal numeric character reference
+ input.next(); // x
+ let c = input.la(0);
+ while (
+ ('a' <= c && c <= 'f') ||
+ ('A' <= c && c <= 'F') ||
+ isDigit(input.lac(0))
+ ) {
+ input.next();
+ c = input.la(0);
+ }
+ if (input.la(0) === ';') {
+ input.next();
+ } else {
+ input.rewind(pos);
+ return null;
+ }
+ } else if (isDigit(input.lac(0))) {
+ // decimal numeric character reference
+ // consume decimal numbers
+ do {
+ input.next();
+ } while (isDigit(input.lac(0)));
+ // check for final ";"
+ if (input.la(0) === ';') {
+ input.next();
+ } else {
+ input.rewind(pos);
+ return null;
+ }
+ } else {
+ input.rewind(pos);
+ return null;
+ }
+ } else {
+ // match named character reference
+ while (isAlpha(input.lac(0))) {
+ input.next();
+ }
+ if (input.la(0) === ';') {
+ input.next();
+ } else {
+ input.rewind(pos);
+ return null;
+ }
+ }
+ return this.createToken(TokenTypes.ENTITY, pos);
+ }
+
+ matchSymbol(pos) {
+ let input = this.input,
+ inElement = this.state === State.ELEMENT,
+ c;
+ while (
+ (c = input.lac(0)) &&
+ (c === 95 ||
+ isAlpha(c) ||
+ isDigit(c) ||
+ (inElement && (c === 45 || c === 58)))
+ ) {
+ input.next();
+ }
+ var end = input.mark();
+ if (pos.index === end.index) {
+ return this.error(
+ 'Expected an Identifier',
+ pos,
+ inElement
+ ? `Expected a valid attribute name, but instead found "${input.la(
+ 0,
+ )}", which is not part of a valid attribute name.`
+ : `Expected letter, digit or underscore but found ${input.la(
+ 0,
+ )} instead.`,
+ );
+ }
+ return this.createToken(TokenTypes.SYMBOL, pos);
+ }
+
+ matchString(pos, allowInterpolation = true) {
+ const input = this.input,
+ start = this.state === State.STRING_SINGLE ? "'" : '"';
+ let c;
+ // string starts with an interpolation
+ if (allowInterpolation && input.la(0) === '#' && input.la(1) === '{') {
+ this.pushState(State.INTERPOLATION);
+ input.next();
+ input.next();
+ return this.createToken(TokenTypes.INTERPOLATION_START, pos);
+ }
+ if (input.la(0) === start) {
+ input.next();
+ this.popState();
+ return this.createToken(TokenTypes.STRING_END, pos);
+ }
+ while ((c = input.la(0)) !== start && c !== EOF) {
+ if (c === '\\' && input.la(1) === start) {
+ // escape sequence for string start
+ input.next();
+ input.next();
+ } else if (allowInterpolation && c === '#' && input.la(1) === '{') {
+ // found interpolation start, string part matched
+ // next iteration will match the interpolation
+ break;
+ } else {
+ input.next();
+ }
+ }
+ var result = this.createToken(TokenTypes.STRING, pos);
+ result.text = result.text.replace('\\', '');
+ return result;
+ }
+
+ matchAttributeValue(pos) {
+ let input = this.input,
+ start = this.state === State.STRING_SINGLE ? "'" : '"',
+ c;
+ if (input.la(0) === '{') {
+ return this.matchExpressionToken(pos);
+ }
+ while ((c = input.la(0)) !== start && c !== EOF) {
+ if (c === '\\' && input.la(1) === start) {
+ input.next();
+ input.next();
+ } else if (c === '{') {
+ // interpolation start
+ break;
+ } else if (c === start) {
+ break;
+ } else {
+ input.next();
+ }
+ }
+ var result = this.createToken(TokenTypes.STRING, pos);
+ result.text = result.text.replace('\\', '');
+ return result;
+ }
+
+ matchNumber(pos) {
+ let input = this.input,
+ c;
+ while ((c = input.lac(0)) !== EOF) {
+ if (!isDigit(c)) {
+ break;
+ }
+ input.next();
+ }
+ if (input.la(0) === '.') {
+ input.next();
+ while ((c = input.lac(0)) !== EOF) {
+ if (!isDigit(c)) {
+ break;
+ }
+ input.next();
+ }
+ }
+ return this.createToken(TokenTypes.NUMBER, pos);
+ }
+
+ matchText(pos) {
+ let input = this.input,
+ exit = false,
+ c;
+ while (!exit && ((c = input.la(0)) && c !== EOF)) {
+ if (c === '{') {
+ const c2 = input.la(1);
+ if (c2 === '{' || c2 === '#' || c2 === '%') {
+ break;
+ }
+ } else if (c === '<') {
+ if (input.la(1) === '/' || isAlpha(input.lac(1))) {
+ break;
+ } else if (input.la(1) === '{') {
+ const c2 = input.la(1);
+ if (c2 === '{' || c2 === '#' || c2 === '%') {
+ break;
+ }
+ }
+ }
+ input.next();
+ }
+ return this.createToken(TokenTypes.TEXT, pos);
+ }
+
+ matchComment(pos) {
+ let input = this.input,
+ c;
+ while ((c = input.next()) !== EOF) {
+ if (c === '#' && input.la(0) === '}') {
+ input.next(); // consume '}'
+ break;
+ }
+ }
+ return this.createToken(TokenTypes.COMMENT, pos);
+ }
+}
+
+function isWhitespace(c) {
+ return c === '\n' || c === ' ' || c === '\t';
+}
+
+function isAlpha(c) {
+ return (65 <= c && c <= 90) || (97 <= c && c <= 122);
+}
+
+function isDigit(c) {
+ return 48 <= c && c <= 57;
+}
diff --git a/packages/melody-parser/src/Parser.js b/packages/melody-parser/src/Parser.js
new file mode 100644
index 0000000..ebb85a8
--- /dev/null
+++ b/packages/melody-parser/src/Parser.js
@@ -0,0 +1,707 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as n from 'melody-types';
+import * as Types from './TokenTypes';
+import { LEFT, RIGHT } from './Associativity';
+import {
+ setStartFromToken,
+ setEndFromToken,
+ copyStart,
+ copyEnd,
+ copyLoc,
+ createNode,
+} from './util';
+import { voidElements } from './elementInfo';
+import * as he from 'he';
+
+type UnaryOperator = {
+ text: String,
+ precendence: Number,
+ createNode: Function,
+};
+
+type BinaryOperator = {
+ text: String,
+ precendence: Number,
+ createNode: Function,
+ associativity: LEFT | RIGHT,
+ parse: Function,
+};
+
+const UNARY = Symbol(),
+ BINARY = Symbol(),
+ TAG = Symbol(),
+ TEST = Symbol();
+export default class Parser {
+ constructor(tokenStream) {
+ this.tokens = tokenStream;
+ this[UNARY] = {};
+ this[BINARY] = {};
+ this[TAG] = {};
+ this[TEST] = {};
+ }
+
+ addUnaryOperator(op: UnaryOperator) {
+ this[UNARY][op.text] = op;
+ return this;
+ }
+
+ addBinaryOperator(op: BinaryOperator) {
+ this[BINARY][op.text] = op;
+ return this;
+ }
+
+ addTag(tag) {
+ this[TAG][tag.name] = tag;
+ return this;
+ }
+
+ addTest(test) {
+ this[TEST][test.text] = test;
+ }
+
+ hasTest(test) {
+ return !!this[TEST][test];
+ }
+
+ getTest(test) {
+ return this[TEST][test];
+ }
+
+ isUnary(token) {
+ return token.type === Types.OPERATOR && !!this[UNARY][token.text];
+ }
+
+ getBinaryOperator(token) {
+ return token.type === Types.OPERATOR && this[BINARY][token.text];
+ }
+
+ parse(test = null) {
+ let tokens = this.tokens,
+ p = setStartFromToken(new n.SequenceExpression(), tokens.la(0));
+ while (!tokens.test(Types.EOF)) {
+ const token = tokens.next();
+ if (!p) {
+ p = setStartFromToken(new n.SequenceExpression(), token);
+ }
+ if (test && test(tokens.la(0).text, token, tokens)) {
+ setEndFromToken(p, token);
+ return p;
+ }
+ switch (token.type) {
+ case Types.EXPRESSION_START: {
+ const expression = this.matchExpression();
+ p.add(
+ copyLoc(
+ new n.PrintExpressionStatement(expression),
+ expression,
+ ),
+ );
+ setEndFromToken(p, tokens.expect(Types.EXPRESSION_END));
+ break;
+ }
+ case Types.TAG_START:
+ p.add(this.matchTag());
+ break;
+ case Types.TEXT:
+ p.add(
+ createNode(
+ n.PrintTextStatement,
+ token,
+ createNode(n.StringLiteral, token, token.text),
+ ),
+ );
+ break;
+ case Types.ENTITY:
+ p.add(
+ createNode(
+ n.PrintTextStatement,
+ token,
+ createNode(
+ n.StringLiteral,
+ token,
+ he.decode(token.text),
+ ),
+ ),
+ );
+ break;
+ case Types.ELEMENT_START:
+ p.add(this.matchElement());
+ break;
+ }
+ }
+ return p;
+ }
+
+ /**
+ * matchElement = '<' SYMBOL attributes* '/'? '>' (children)* '<' '/' SYMBOL '>'
+ * attributes = SYMBOL '=' (matchExpression | matchString)
+ * | matchExpression
+ */
+ matchElement() {
+ let tokens = this.tokens,
+ elementStartToken = tokens.la(0),
+ elementName,
+ element;
+ if (!(elementName = tokens.nextIf(Types.SYMBOL))) {
+ this.error({
+ title: 'Expected element start',
+ pos: elementStartToken.pos,
+ advice:
+ tokens.lat(0) === Types.SLASH
+ ? `Unexpected closing "${tokens.la(1)
+ .text}" tag. Seems like your DOM is out of control.`
+ : 'Expected an element to start',
+ });
+ }
+
+ element = new n.Element(elementName.text);
+ setStartFromToken(element, elementStartToken);
+
+ this.matchAttributes(element, tokens);
+
+ if (tokens.nextIf(Types.SLASH)) {
+ tokens.expect(Types.ELEMENT_END);
+ element.selfClosing = true;
+ } else {
+ tokens.expect(Types.ELEMENT_END);
+ if (voidElements[elementName.text]) {
+ element.selfClosing = true;
+ } else {
+ element.children = this.parse(function(_, token, tokens) {
+ if (
+ token.type === Types.ELEMENT_START &&
+ tokens.lat(0) === Types.SLASH
+ ) {
+ const name = tokens.la(1);
+ if (
+ name.type === Types.SYMBOL &&
+ name.text === elementName.text
+ ) {
+ tokens.next(); // SLASH
+ tokens.next(); // elementName
+ tokens.expect(Types.ELEMENT_END);
+ return true;
+ }
+ }
+ return false;
+ }).expressions;
+ }
+ }
+ setEndFromToken(element, tokens.la(-1));
+ return element;
+ }
+
+ matchAttributes(element, tokens) {
+ while (
+ tokens.lat(0) !== Types.SLASH &&
+ tokens.lat(0) !== Types.ELEMENT_END
+ ) {
+ const key = tokens.nextIf(Types.SYMBOL);
+ if (key) {
+ const keyNode = new n.Identifier(key.text);
+ setStartFromToken(keyNode, key);
+ setEndFromToken(keyNode, key);
+
+ // match an attribute
+ if (tokens.nextIf(Types.ASSIGNMENT)) {
+ const start = tokens.expect(Types.STRING_START);
+ let canBeString = true,
+ nodes = [],
+ token;
+ while (!tokens.test(Types.STRING_END)) {
+ if (
+ canBeString &&
+ (token = tokens.nextIf(Types.STRING))
+ ) {
+ nodes[nodes.length] = createNode(
+ n.StringLiteral,
+ token,
+ token.text,
+ );
+ canBeString = false;
+ } else if (
+ (token = tokens.nextIf(Types.EXPRESSION_START))
+ ) {
+ nodes[nodes.length] = this.matchExpression();
+ tokens.expect(Types.EXPRESSION_END);
+ canBeString = true;
+ } else {
+ break;
+ }
+ }
+ tokens.expect(Types.STRING_END);
+ if (!nodes.length) {
+ nodes.push(createNode(n.StringLiteral, start, ''));
+ }
+
+ let expr = nodes[0];
+ for (let i = 1, len = nodes.length; i < len; i++) {
+ const { line, column } = expr.loc.start;
+ expr = new n.BinaryConcatExpression(expr, nodes[i]);
+ expr.loc.start.line = line;
+ expr.loc.start.column = column;
+ copyEnd(expr, expr.right);
+ }
+ const attr = new n.Attribute(keyNode, expr);
+ copyStart(attr, keyNode);
+ copyEnd(attr, expr);
+ element.attributes.push(attr);
+ } else {
+ element.attributes.push(
+ copyLoc(new n.Attribute(keyNode), keyNode),
+ );
+ }
+ } else if (tokens.nextIf(Types.EXPRESSION_START)) {
+ element.attributes.push(this.matchExpression());
+ tokens.expect(Types.EXPRESSION_END);
+ } else {
+ this.error({
+ title: 'Invalid token',
+ pos: tokens.la(0).pos,
+ advice:
+ 'A tag must consist of attributes or expressions. Twig Tags are not allowed.',
+ });
+ }
+ }
+ }
+
+ error(options) {
+ this.tokens.error(options.title, options.pos, options.advice);
+ }
+
+ matchTag() {
+ let tokens = this.tokens,
+ tag = tokens.expect(Types.SYMBOL),
+ parser = this[TAG][tag.text];
+ if (!parser) {
+ tokens.error(
+ `Unknown tag "${tag.text}"`,
+ tag.pos,
+ `Expected a known tag such as\n- ${Object.getOwnPropertyNames(
+ this[TAG],
+ ).join('\n- ')}`,
+ tag.length,
+ );
+ }
+ return parser.parse(this, tag);
+ }
+
+ matchExpression(precedence = 0) {
+ let expr = this.getPrimary(),
+ tokens = this.tokens,
+ token,
+ op;
+ while (
+ (token = tokens.la(0)) &&
+ token.type !== Types.EOF &&
+ (op = this.getBinaryOperator(token)) &&
+ op.precedence >= precedence
+ ) {
+ const opToken = tokens.next(); // consume the operator
+ if (op.parse) {
+ expr = op.parse(this, opToken, expr);
+ } else {
+ const expr1 = this.matchExpression(
+ op.associativity === LEFT
+ ? op.precedence + 1
+ : op.precedence,
+ );
+ expr = op.createNode(token, expr, expr1);
+ }
+ token = tokens.la(0);
+ }
+
+ return precedence === 0 ? this.matchConditionalExpression(expr) : expr;
+ }
+
+ getPrimary() {
+ let tokens = this.tokens,
+ token = tokens.la(0);
+ if (this.isUnary(token)) {
+ const op = this[UNARY][token.text];
+ tokens.next(); // consume operator
+ const expr = this.matchExpression(op.precedence);
+ return this.matchPostfixExpression(op.createNode(token, expr));
+ } else if (tokens.test(Types.LPAREN)) {
+ tokens.next(); // consume '('
+ const expr = this.matchExpression();
+ tokens.expect(Types.RPAREN);
+ return this.matchPostfixExpression(expr);
+ }
+
+ return this.matchPrimaryExpression();
+ }
+
+ matchPrimaryExpression() {
+ let tokens = this.tokens,
+ token = tokens.la(0),
+ node;
+ switch (token.type) {
+ case Types.NULL:
+ node = createNode(n.NullLiteral, tokens.next());
+ break;
+ case Types.FALSE:
+ node = createNode(n.BooleanLiteral, tokens.next(), false);
+ break;
+ case Types.TRUE:
+ node = createNode(n.BooleanLiteral, tokens.next(), true);
+ break;
+ case Types.SYMBOL:
+ tokens.next();
+ if (tokens.test(Types.LPAREN)) {
+ // SYMBOL '(' arguments* ')'
+ node = new n.CallExpression(
+ createNode(n.Identifier, token, token.text),
+ this.matchArguments(),
+ );
+ copyStart(node, node.callee);
+ setEndFromToken(node, tokens.la(-1)); // ')'
+ } else {
+ node = createNode(n.Identifier, token, token.text);
+ }
+ break;
+ case Types.NUMBER:
+ node = createNode(
+ n.NumericLiteral,
+ token,
+ Number(tokens.next()),
+ );
+ break;
+ case Types.STRING_START:
+ node = this.matchStringExpression();
+ break;
+ // potentially missing: OPERATOR type
+ default:
+ if (token.type === Types.LBRACE) {
+ node = this.matchArray();
+ } else if (token.type === Types.LBRACKET) {
+ node = this.matchMap();
+ } else {
+ this.error({
+ title:
+ 'Unexpected token "' +
+ token.type +
+ '" of value "' +
+ token.text +
+ '"',
+ pos: token.pos,
+ });
+ }
+ break;
+ }
+
+ return this.matchPostfixExpression(node);
+ }
+
+ matchStringExpression() {
+ let tokens = this.tokens,
+ nodes = [],
+ canBeString = true,
+ token,
+ stringStart,
+ stringEnd;
+ stringStart = tokens.expect(Types.STRING_START);
+ while (!tokens.test(Types.STRING_END)) {
+ if (canBeString && (token = tokens.nextIf(Types.STRING))) {
+ nodes[nodes.length] = createNode(
+ n.StringLiteral,
+ token,
+ token.text,
+ );
+ canBeString = false;
+ } else if ((token = tokens.nextIf(Types.INTERPOLATION_START))) {
+ nodes[nodes.length] = this.matchExpression();
+ tokens.expect(Types.INTERPOLATION_END);
+ canBeString = true;
+ } else {
+ break;
+ }
+ }
+ stringEnd = tokens.expect(Types.STRING_END);
+
+ if (!nodes.length) {
+ return setEndFromToken(
+ createNode(n.StringLiteral, stringStart, ''),
+ stringEnd,
+ );
+ }
+
+ let expr = nodes[0];
+ for (let i = 1, len = nodes.length; i < len; i++) {
+ const { line, column } = expr.loc.start;
+ expr = new n.BinaryConcatExpression(expr, nodes[i]);
+ expr.loc.start.line = line;
+ expr.loc.start.column = column;
+ copyEnd(expr, expr.right);
+ }
+
+ return expr;
+ }
+
+ matchConditionalExpression(test: Node) {
+ let tokens = this.tokens,
+ condition = test,
+ consequent,
+ alternate;
+ while (tokens.nextIf(Types.QUESTION_MARK)) {
+ if (!tokens.nextIf(Types.COLON)) {
+ consequent = this.matchExpression();
+ if (tokens.nextIf(Types.COLON)) {
+ alternate = this.matchExpression();
+ } else {
+ alternate = null;
+ }
+ } else {
+ consequent = null;
+ alternate = this.matchExpression();
+ }
+ const { line, column } = condition.loc.start;
+ condition = new n.ConditionalExpression(
+ condition,
+ consequent,
+ alternate,
+ );
+ condition.loc.start = { line, column };
+ copyEnd(condition, alternate || consequent);
+ }
+ return condition;
+ }
+
+ matchArray() {
+ let tokens = this.tokens,
+ array = new n.ArrayExpression(),
+ start = tokens.expect(Types.LBRACE);
+ setStartFromToken(array, start);
+ while (!tokens.test(Types.RBRACE) && !tokens.test(Types.EOF)) {
+ array.elements.push(this.matchExpression());
+ if (!tokens.test(Types.RBRACE)) {
+ tokens.expect(Types.COMMA);
+ // support trailing commas
+ if (tokens.test(Types.RBRACE)) {
+ break;
+ }
+ }
+ }
+ setEndFromToken(array, tokens.expect(Types.RBRACE));
+ return array;
+ }
+
+ matchMap() {
+ let tokens = this.tokens,
+ token,
+ obj = new n.ObjectExpression(),
+ startToken = tokens.expect(Types.LBRACKET);
+ setStartFromToken(obj, startToken);
+ while (!tokens.test(Types.RBRACKET) && !tokens.test(Types.EOF)) {
+ let computed = false,
+ key,
+ value;
+ if (tokens.test(Types.STRING_START)) {
+ key = this.matchStringExpression();
+ if (!n.is('StringLiteral', key)) {
+ computed = true;
+ }
+ } else if ((token = tokens.nextIf(Types.SYMBOL))) {
+ key = createNode(n.Identifier, token, token.text);
+ } else if ((token = tokens.nextIf(Types.NUMBER))) {
+ key = createNode(n.NumericLiteral, token, Number(token.text));
+ } else if (tokens.test(Types.LPAREN)) {
+ key = this.matchExpression();
+ computed = true;
+ } else {
+ this.error({
+ title: 'Invalid map key',
+ pos: tokens.la(0).pos,
+ advice:
+ 'Key must be a string, symbol or a number but was ' +
+ tokens.next(),
+ });
+ }
+ tokens.expect(Types.COLON);
+ value = this.matchExpression();
+ const prop = new n.ObjectProperty(key, value, computed);
+ copyStart(prop, key);
+ copyEnd(prop, value);
+ obj.properties.push(prop);
+ if (!tokens.test(Types.RBRACKET)) {
+ tokens.expect(Types.COMMA);
+ // support trailing comma
+ if (tokens.test(Types.RBRACKET)) {
+ break;
+ }
+ }
+ }
+ setEndFromToken(obj, tokens.expect(Types.RBRACKET));
+ return obj;
+ }
+
+ matchPostfixExpression(expr) {
+ const tokens = this.tokens;
+ let node = expr;
+ while (!tokens.test(Types.EOF)) {
+ if (tokens.test(Types.DOT) || tokens.test(Types.LBRACE)) {
+ node = this.matchSubscriptExpression(node);
+ } else if (tokens.test(Types.PIPE)) {
+ tokens.next();
+ node = this.matchFilterExpression(node);
+ } else {
+ break;
+ }
+ }
+
+ return node;
+ }
+
+ matchSubscriptExpression(node) {
+ let tokens = this.tokens,
+ op = tokens.next();
+ if (op.type === Types.DOT) {
+ let token = tokens.next(),
+ computed = false,
+ property;
+ if (token.type === Types.SYMBOL) {
+ property = createNode(n.Identifier, token, token.text);
+ } else if (token.type === Types.NUMBER) {
+ property = createNode(
+ n.NumericLiteral,
+ token,
+ Number(token.text),
+ );
+ computed = true;
+ } else {
+ this.error({
+ title: 'Invalid token',
+ pos: token.pos,
+ advice:
+ 'Expected number or symbol, found ' +
+ token +
+ ' instead',
+ });
+ }
+
+ const memberExpr = new n.MemberExpression(node, property, computed);
+ copyStart(memberExpr, node);
+ copyEnd(memberExpr, property);
+ if (tokens.test(Types.LPAREN)) {
+ const callExpr = new n.CallExpression(
+ memberExpr,
+ this.matchArguments(),
+ );
+ copyStart(callExpr, memberExpr);
+ setEndFromToken(callExpr, tokens.la(-1));
+ return callExpr;
+ }
+ return memberExpr;
+ } else {
+ let arg, start;
+ if (tokens.test(Types.COLON)) {
+ // slice
+ tokens.next();
+ start = null;
+ } else {
+ arg = this.matchExpression();
+ if (tokens.test(Types.COLON)) {
+ start = arg;
+ arg = null;
+ tokens.next();
+ }
+ }
+
+ if (arg) {
+ return setEndFromToken(
+ copyStart(new n.MemberExpression(node, arg, true), node),
+ tokens.expect(Types.RBRACE),
+ );
+ } else {
+ // slice
+ const result = new n.SliceExpression(
+ node,
+ start,
+ tokens.test(Types.RBRACE) ? null : this.matchExpression(),
+ );
+ copyStart(result, node);
+ setEndFromToken(result, tokens.expect(Types.RBRACE));
+ return result;
+ }
+ }
+ }
+
+ matchFilterExpression(node) {
+ let tokens = this.tokens,
+ target = node;
+ while (!tokens.test(Types.EOF)) {
+ let token = tokens.expect(Types.SYMBOL),
+ name = createNode(n.Identifier, token, token.text),
+ args;
+ if (tokens.test(Types.LPAREN)) {
+ args = this.matchArguments();
+ } else {
+ args = [];
+ }
+ const newTarget = new n.FilterExpression(target, name, args);
+ copyStart(newTarget, target);
+ if (newTarget.arguments.length) {
+ copyEnd(
+ newTarget,
+ newTarget.arguments[newTarget.arguments.length - 1],
+ );
+ } else {
+ copyEnd(newTarget, target);
+ }
+ target = newTarget;
+
+ if (!tokens.test(Types.PIPE) || tokens.test(Types.EOF)) {
+ break;
+ }
+
+ tokens.next(); // consume '|'
+ }
+ return target;
+ }
+
+ matchArguments() {
+ let tokens = this.tokens,
+ args = [];
+ tokens.expect(Types.LPAREN);
+ while (!tokens.test(Types.RPAREN) && !tokens.test(Types.EOF)) {
+ if (
+ tokens.test(Types.SYMBOL) &&
+ tokens.lat(1) === Types.ASSIGNMENT
+ ) {
+ const name = tokens.next();
+ tokens.next();
+ const value = this.matchExpression();
+ const arg = new n.NamedArgumentExpression(
+ createNode(n.Identifier, name, name.text),
+ value,
+ );
+ copyEnd(arg, value);
+ args.push(arg);
+ } else {
+ args.push(this.matchExpression());
+ }
+
+ if (!tokens.test(Types.COMMA)) {
+ tokens.expect(Types.RPAREN);
+ return args;
+ }
+ tokens.expect(Types.COMMA);
+ }
+ tokens.expect(Types.RPAREN);
+ return args;
+ }
+}
diff --git a/packages/melody-parser/src/TokenStream.js b/packages/melody-parser/src/TokenStream.js
new file mode 100644
index 0000000..05427c1
--- /dev/null
+++ b/packages/melody-parser/src/TokenStream.js
@@ -0,0 +1,177 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ EOF_TOKEN,
+ ERROR,
+ ERROR_TABLE,
+ COMMENT,
+ WHITESPACE,
+ HTML_COMMENT,
+ TAG_START,
+ TAG_END,
+ EXPRESSION_START,
+ EXPRESSION_END,
+ TEXT,
+ STRING,
+} from './TokenTypes';
+import trimEnd from 'lodash/trimEnd';
+import trimStart from 'lodash/trimStart';
+import codeFrame from 'melody-code-frame';
+
+const TOKENS = Symbol(),
+ LENGTH = Symbol();
+
+export default class TokenStream {
+ constructor(
+ lexer,
+ options = { ignoreComments: true, ignoreWhitespace: true },
+ ) {
+ this.input = lexer;
+ this.index = 0;
+ this.options = options;
+ this[TOKENS] = getAllTokens(lexer, options);
+ this[LENGTH] = this[TOKENS].length;
+
+ if (
+ this[TOKENS].length &&
+ this[TOKENS][this[TOKENS].length - 1].type === ERROR
+ ) {
+ const errorToken = this[TOKENS][this[TOKENS].length - 1];
+ this.error(
+ errorToken.message,
+ errorToken.pos,
+ errorToken.advice,
+ errorToken.endPos.index - errorToken.pos.index || 1,
+ );
+ }
+ }
+
+ la(offset) {
+ var index = this.index + offset;
+ return index < this[LENGTH] ? this[TOKENS][index] : EOF_TOKEN;
+ }
+
+ lat(offset) {
+ return this.la(offset).type;
+ }
+
+ test(type, text) {
+ const token = this.la(0);
+ return token.type === type && (!text || token.text === text);
+ }
+
+ next() {
+ if (this.index === this[LENGTH]) {
+ return EOF_TOKEN;
+ }
+ const token = this[TOKENS][this.index];
+ this.index++;
+ return token;
+ }
+
+ nextIf(type, text) {
+ if (this.test(type, text)) {
+ return this.next();
+ }
+ return false;
+ }
+
+ expect(type, text) {
+ const token = this.la(0);
+ if (token.type === type && (!text || token.text === text)) {
+ return this.next();
+ }
+ this.error(
+ 'Invalid Token',
+ token.pos,
+ `Expected ${ERROR_TABLE[type] ||
+ type ||
+ text} but found ${ERROR_TABLE[token.type] ||
+ token.type ||
+ token.text} instead.`,
+ token.length,
+ );
+ }
+
+ error(message, pos, advice, length = 1) {
+ let errorMessage = `ERROR: ${message}\n`;
+ errorMessage += codeFrame({
+ rawLines: this.input.source,
+ lineNumber: pos.line,
+ colNumber: pos.column,
+ length,
+ tokens: getAllTokens(this.input, {
+ ignoreWhitespace: false,
+ ignoreComments: false,
+ ignoreHtmlComments: false,
+ }),
+ });
+ if (advice) {
+ errorMessage += '\n\n' + advice;
+ }
+ throw new Error(errorMessage);
+ }
+}
+
+function getAllTokens(lexer, options) {
+ let token,
+ tokens = [],
+ acceptWhitespaceControl = false,
+ trimNext = false;
+ while ((token = lexer.next()) !== EOF_TOKEN) {
+ const shouldTrimNext = trimNext;
+ trimNext = false;
+ if (acceptWhitespaceControl) {
+ switch (token.type) {
+ case EXPRESSION_START:
+ case TAG_START:
+ if (token.text[token.text.length - 1] === '-') {
+ tokens[tokens.length - 1].text = trimEnd(
+ tokens[tokens.length - 1].text,
+ );
+ }
+ break;
+ case EXPRESSION_END:
+ case TAG_END:
+ if (token.text[0] === '-') {
+ trimNext = true;
+ }
+ break;
+ case COMMENT:
+ if (tokens[tokens.length - 1].type === TEXT) {
+ tokens[tokens.length - 1].text = trimEnd(tokens.text);
+ }
+ trimNext = true;
+ break;
+ }
+ }
+ if (shouldTrimNext && (token.type === TEXT || token.type === STRING)) {
+ token.text = trimStart(token.text);
+ }
+ if (
+ (token.type !== COMMENT || !options.ignoreComments) &&
+ (token.type !== WHITESPACE || !options.ignoreWhitespace) &&
+ (token.type !== HTML_COMMENT || !options.ignoreHtmlComments)
+ ) {
+ tokens[tokens.length] = token;
+ }
+ acceptWhitespaceControl = true;
+ if (token.type === ERROR) {
+ return tokens;
+ }
+ }
+ return tokens;
+}
diff --git a/packages/melody-parser/src/TokenTypes.js b/packages/melody-parser/src/TokenTypes.js
new file mode 100644
index 0000000..639c112
--- /dev/null
+++ b/packages/melody-parser/src/TokenTypes.js
@@ -0,0 +1,73 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const EXPRESSION_START = 'expressionStart';
+export const EXPRESSION_END = 'expressionEnd';
+export const TAG_START = 'tagStart';
+export const TAG_END = 'tagEnd';
+export const INTERPOLATION_START = 'interpolationStart';
+export const INTERPOLATION_END = 'interpolationEnd';
+export const STRING_START = 'stringStart';
+export const STRING_END = 'stringEnd';
+export const COMMENT = 'comment';
+export const WHITESPACE = 'whitespace';
+export const HTML_COMMENT = 'htmlComment';
+export const TEXT = 'text';
+export const ENTITY = 'entity';
+export const SYMBOL = 'symbol';
+export const STRING = 'string';
+export const OPERATOR = 'operator';
+export const TRUE = 'true';
+export const FALSE = 'false';
+export const NULL = 'null';
+export const LBRACE = '[';
+export const RBRACE = ']';
+export const LPAREN = '(';
+export const RPAREN = ')';
+export const LBRACKET = '{';
+export const RBRACKET = '}';
+export const COLON = ':';
+export const COMMA = ',';
+export const DOT = '.';
+export const PIPE = '|';
+export const QUESTION_MARK = '?';
+export const ASSIGNMENT = '=';
+export const ELEMENT_START = '<';
+export const SLASH = '/';
+export const ELEMENT_END = '>';
+export const NUMBER = 'number';
+export const EOF = 'EOF';
+export const ERROR = 'ERROR';
+export const EOF_TOKEN = {
+ type: EOF,
+ pos: {
+ index: -1,
+ line: -1,
+ pos: -1,
+ },
+ end: -1,
+ length: 0,
+ source: null,
+ text: '',
+};
+
+export const ERROR_TABLE = {
+ [EXPRESSION_END]: 'expression end "}}"',
+ [EXPRESSION_START]: 'expression start "{{"',
+ [TAG_START]: 'tag start "{%"',
+ [TAG_END]: 'tag end "%}"',
+ [INTERPOLATION_START]: 'interpolation start "#{"',
+ [INTERPOLATION_END]: 'interpolation end "}"',
+};
diff --git a/packages/melody-parser/src/elementInfo.js b/packages/melody-parser/src/elementInfo.js
new file mode 100644
index 0000000..a3f3fc6
--- /dev/null
+++ b/packages/melody-parser/src/elementInfo.js
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// https://www.w3.org/TR/html5/syntax.html#void-elements
+export const voidElements = {
+ area: true,
+ base: true,
+ br: true,
+ col: true,
+ embed: true,
+ hr: true,
+ img: true,
+ input: true,
+ keygen: true,
+ link: true,
+ meta: true,
+ param: true,
+ source: true,
+ track: true,
+ wbr: true,
+};
+
+export const rawTextElements = {
+ script: true,
+ style: true,
+};
+
+export const escapableRawTextElements = {
+ textarea: true,
+ title: true,
+};
diff --git a/packages/melody-parser/src/index.js b/packages/melody-parser/src/index.js
new file mode 100644
index 0000000..25a28e9
--- /dev/null
+++ b/packages/melody-parser/src/index.js
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Parser from './Parser';
+import TokenStream from './TokenStream';
+import * as Types from './TokenTypes';
+import Lexer from './Lexer';
+import { EOF, CharStream } from './CharStream';
+import { LEFT, RIGHT } from './Associativity';
+import {
+ setStartFromToken,
+ setEndFromToken,
+ copyStart,
+ copyEnd,
+ copyLoc,
+ createNode,
+} from './util';
+
+function parse(code) {
+ const p = new Parser(new TokenStream(new Lexer(new CharStream(code))));
+ return p.parse();
+}
+
+export {
+ Parser,
+ TokenStream,
+ Lexer,
+ EOF,
+ CharStream,
+ LEFT,
+ RIGHT,
+ parse,
+ setStartFromToken,
+ setEndFromToken,
+ copyStart,
+ copyEnd,
+ copyLoc,
+ createNode,
+ Types,
+};
diff --git a/packages/melody-parser/src/util.js b/packages/melody-parser/src/util.js
new file mode 100644
index 0000000..98acc03
--- /dev/null
+++ b/packages/melody-parser/src/util.js
@@ -0,0 +1,56 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export function setStartFromToken(node, { pos: { index, line, column } }) {
+ node.loc.start = { line, column, index };
+ return node;
+}
+
+export function setEndFromToken(node, { pos: { line, column }, end }) {
+ node.loc.end = { line, column, index: end };
+ return node;
+}
+
+export function copyStart(node, { loc: { start: { line, column, index } } }) {
+ node.loc.start.line = line;
+ node.loc.start.column = column;
+ node.loc.start.index = index;
+ return node;
+}
+
+export function copyEnd(node, end) {
+ node.loc.end.line = end.loc.end.line;
+ node.loc.end.column = end.loc.end.column;
+ node.loc.end.index = end.loc.end.index;
+ return node;
+}
+
+export function copyLoc(node, { loc: { start, end } }) {
+ node.loc.start.line = start.line;
+ node.loc.start.column = start.column;
+ node.loc.start.index = start.index;
+ node.loc.end.line = end.line;
+ node.loc.end.column = end.column;
+ node.loc.end.index = end.index;
+ return node;
+}
+
+export function createNode(Type, token, ...args) {
+ return setEndFromToken(setStartFromToken(new Type(...args), token), token);
+}
+
+export function startNode(Type, token, ...args) {
+ return setStartFromToken(new Type(...args), token);
+}
diff --git a/packages/melody-plugin-idom/package.json b/packages/melody-plugin-idom/package.json
new file mode 100644
index 0000000..4905b8e
--- /dev/null
+++ b/packages/melody-plugin-idom/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "melody-plugin-idom",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; SUPPORT_CJS=true rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "babel-types": "^6.8.1"
+ },
+ "bundledDependencies": [
+ "babel-types"
+ ],
+ "peerDependencies": {
+ "melody-types": "^0.10.0"
+ },
+ "devDependencies": {
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-plugin-idom/src/index.js b/packages/melody-plugin-idom/src/index.js
new file mode 100644
index 0000000..04ef783
--- /dev/null
+++ b/packages/melody-plugin-idom/src/index.js
@@ -0,0 +1,50 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+import mountVisitor from './visitors/mount.js';
+import idomVisitor from './visitors/idom.js';
+
+export default {
+ visitors: [idomVisitor, mountVisitor],
+ filterMap: {
+ raw(path) {
+ if (path.parentPath.is('PrintStatement')) {
+ path.replaceWithJS(
+ t.expressionStatement(
+ t.callExpression(
+ t.identifier(
+ path.state.addImportFrom('melody-idom', 'raw'),
+ ),
+ [path.get('target').node],
+ ),
+ ),
+ );
+ } else {
+ path.replaceWithJS(
+ t.callExpression(
+ t.identifier(
+ path.state.addImportFrom(
+ 'melody-idom',
+ 'rawString',
+ ),
+ ),
+ [path.get('target').node],
+ ),
+ );
+ }
+ },
+ },
+};
diff --git a/packages/melody-plugin-idom/src/visitors/idom.js b/packages/melody-plugin-idom/src/visitors/idom.js
new file mode 100644
index 0000000..52a904a
--- /dev/null
+++ b/packages/melody-plugin-idom/src/visitors/idom.js
@@ -0,0 +1,544 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+import { Node } from 'melody-types';
+
+export default {
+ convert: {
+ PrintStatement: {
+ exit(path) {
+ const value = path.get('value');
+ path.replaceWithJS(
+ value.is('ExpressionStatement')
+ ? value.node
+ : callIdomFunction(this, 'text', [value.node]),
+ );
+ },
+ },
+ Element: {
+ exit(path) {
+ const el = path.node,
+ isSelfClosing = el.selfClosing || !el.children.length;
+
+ if (isSelfClosing && !el.attributes.length) {
+ // empty element
+ path.replaceWithJS(
+ openElementWithoutAttributes(
+ this,
+ 'elementVoid',
+ el.name,
+ ),
+ );
+ } else {
+ const replacements = [];
+ let ref;
+ // has no attributes but has children
+ if (!el.attributes.length) {
+ replacements.push(
+ openElementWithoutAttributes(
+ this,
+ 'elementOpen',
+ el.name,
+ ),
+ );
+ } else {
+ // has attributes
+ ref = openElementWithAttributes(
+ this,
+ path,
+ el,
+ replacements,
+ );
+ }
+
+ if (!isSelfClosing) {
+ closeElement(this, ref, replacements, el);
+ }
+ path.replaceWithMultipleJS(...replacements);
+ }
+ },
+ },
+ Fragment: {
+ exit(path) {
+ path.replaceWithJS(t.expressionStatement(path.node.value));
+ },
+ },
+ },
+};
+
+function openElementWithAttributes(state, path, el, replacements) {
+ const staticAttributes = [],
+ attributes = [],
+ dynamicAttributeExpressions = [];
+ let ref;
+ let key;
+ let i = 0;
+ for (const attrs = el.attributes, len = attrs.length; i < len; i++) {
+ const attr = attrs[i];
+ if (!Node.isAttribute(attr)) {
+ dynamicAttributeExpressions.push(attr);
+ } else if (Node.isIdentifier(attr.name) && attr.name.name === 'key') {
+ key = attr.value;
+ } else if (
+ Node.isIdentifier(attr.name) &&
+ attr.name.name === 'ref' &&
+ attr.isImmutable()
+ ) {
+ staticAttributes.push(
+ t.stringLiteral('ref'),
+ t.callExpression(
+ t.identifier(state.addImportFrom('melody-idom', 'ref')),
+ [attr.value],
+ ),
+ );
+ } else if (attr.isImmutable()) {
+ staticAttributes.push(t.stringLiteral(attr.name.name), attr.value);
+ } else {
+ addStaticAttribute(attributes, attr);
+ }
+ }
+
+ key = ensureKeyIsValid(state, key, staticAttributes.length);
+
+ const staticId = getStaticId(state, path, staticAttributes);
+ const openElement = dynamicAttributeExpressions.length
+ ? openDynamicAttributesElement
+ : openSimpleElement;
+
+ openElement(
+ state,
+ path,
+ ref,
+ key,
+ staticId,
+ attributes,
+ dynamicAttributeExpressions,
+ replacements,
+ );
+ return ref;
+}
+
+function ensureKeyIsValid(state, maybeKey, hasStaticAttributes) {
+ if (maybeKey) {
+ return Node.isStringLiteral(maybeKey)
+ ? maybeKey
+ : t.binaryExpression('+', t.stringLiteral(''), maybeKey);
+ }
+
+ if (hasStaticAttributes && state.options.generateKey) {
+ return t.stringLiteral(state.generateKey());
+ }
+
+ return t.nullLiteral();
+}
+
+function getStaticId(state, path, staticAttributes) {
+ let staticId;
+ if (staticAttributes.length) {
+ const staticIdName = path.scope.generateUid('statics');
+ staticId = t.identifier(staticIdName);
+
+ path.scope.registerBinding(staticIdName, null, 'global');
+ state.insertGlobalVariableDeclaration(
+ 'const',
+ staticId,
+ t.arrayExpression(staticAttributes),
+ );
+ } else {
+ staticId = t.nullLiteral();
+ }
+ return staticId;
+}
+
+function openSimpleElement(
+ state,
+ path,
+ ref,
+ key,
+ staticId,
+ attributes,
+ dynamicAttributeExpressions,
+ replacements,
+) {
+ const el = path.node;
+ const isSelfClosing = el.selfClosing || !el.children.length;
+ let openElementCall = elementOpen(
+ state,
+ isSelfClosing ? 'elementVoid' : 'elementOpen',
+ el.name,
+ key,
+ staticId,
+ attributes,
+ );
+ if (isSelfClosing && ref) {
+ openElementCall = t.callExpression(
+ t.identifier(state.addImportFrom('melody-idom', 'ref')),
+ [ref, openElementCall],
+ );
+ }
+ replacements.push(t.expressionStatement(openElementCall));
+}
+
+function addStaticAttribute(attributes, attr) {
+ if (Node.isIdentifier(attr.name)) {
+ attributes.push(
+ t.stringLiteral(attr.name.name),
+ attr.value || t.booleanLiteral(true),
+ );
+ } else {
+ attributes.push(attr.name, attr.value || t.nullLiteral());
+ }
+}
+
+function openDynamicAttributesElement(
+ state,
+ path,
+ ref,
+ key,
+ staticId,
+ attributes,
+ dynamicAttributeExpressions,
+ replacements,
+) {
+ const el = path.node;
+ const isSelfClosing = el.selfClosing || !el.children.length;
+
+ // there are dynamic attribute expressions
+ replacements.push(
+ t.expressionStatement(
+ elementOpen(
+ state,
+ 'elementOpenStart',
+ el.name,
+ key,
+ staticId,
+ attributes,
+ ),
+ ),
+ );
+ // todo adjust unit tests to remove this line
+ state.addImportFrom('melody-idom', 'elementOpenEnd');
+
+ addDynamicAttributeCalls(
+ state,
+ path,
+ dynamicAttributeExpressions,
+ replacements,
+ );
+
+ // close the opening tag
+ replacements.push(callIdomFunction(state, 'elementOpenEnd', []));
+
+ if (isSelfClosing) {
+ if (ref) {
+ replacements.push(callIdomFunction(state, 'ref', [ref]));
+ }
+ // we handle closing the tag here since there is
+ // no equivalent of 'elementVoid' when using dynamic attributes
+ replacements.push(
+ callIdomFunction(state, 'elementClose', [t.stringLiteral(el.name)]),
+ );
+ }
+}
+
+function addDynamicAttributeCalls(
+ state,
+ path,
+ dynamicAttributeExpressions,
+ replacements,
+) {
+ let i = 0;
+ const attrFn = t.identifier(state.addImportFrom('melody-idom', 'attr'));
+ for (const len = dynamicAttributeExpressions.length; i < len; i++) {
+ const scope = path.scope;
+ const indexName = scope.generateUid('i');
+ const localIterableName = scope.generateUid('a');
+ const lengthName = scope.generateUid('len');
+
+ scope.registerBinding(indexName, path, 'var');
+ scope.registerBinding(localIterableName, path, 'var');
+ scope.registerBinding(lengthName, path, 'var');
+
+ replacements.push(
+ dynamicAttributes({
+ ATTR: attrFn,
+ INDEX: t.identifier(indexName),
+ LOCAL_ITERABLE: t.identifier(localIterableName),
+ LENGTH: t.identifier(lengthName),
+ ITERABLE: dynamicAttributeExpressions[i],
+ }),
+ );
+ }
+}
+
+function elementOpen(state, openType, name, key, staticId, attributes) {
+ return t.callExpression(
+ t.identifier(state.addImportFrom('melody-idom', openType)),
+ [t.stringLiteral(name), key, staticId, ...attributes],
+ );
+}
+
+function openElementWithoutAttributes(state, openType, name) {
+ return callIdomFunction(state, openType, [
+ t.stringLiteral(name),
+ t.nullLiteral(),
+ t.nullLiteral(),
+ ]);
+}
+
+function callIdomFunction(state, name, args) {
+ return t.expressionStatement(
+ t.callExpression(
+ t.identifier(state.addImportFrom('melody-idom', name)),
+ args,
+ ),
+ );
+}
+
+function closeElement(state, ref, replacements, el) {
+ if (ref) {
+ replacements.push(callIdomFunction(state, 'ref', [ref]));
+ }
+ if (el.children) {
+ replacements.push(...el.children);
+ }
+ replacements.push(
+ callIdomFunction(state, 'elementClose', [t.stringLiteral(el.name)]),
+ );
+}
+
+function dynamicAttributes(ctx) {
+ return {
+ type: 'ForStatement',
+ init: {
+ type: 'VariableDeclaration',
+ declarations: [
+ {
+ type: 'VariableDeclarator',
+ id: ctx.INDEX,
+ init: {
+ type: 'NumericLiteral',
+ extra: {
+ rawValue: 0,
+ raw: '0',
+ },
+ value: 0,
+ },
+ },
+ {
+ type: 'VariableDeclarator',
+ id: ctx.LOCAL_ITERABLE,
+ init: {
+ type: 'ConditionalExpression',
+ test: {
+ type: 'LogicalExpression',
+ left: {
+ type: 'BinaryExpression',
+ left: {
+ type: 'Identifier',
+ name: 'process.env.NODE_ENV',
+ },
+ operator: '===',
+ right: {
+ type: 'StringLiteral',
+ extra: {
+ rawValue: 'production',
+ raw: '"production"',
+ },
+ value: 'production',
+ },
+ },
+ operator: '||',
+ right: {
+ type: 'CallExpression',
+ callee: {
+ type: 'MemberExpression',
+ object: {
+ type: 'Identifier',
+ name: 'Array',
+ },
+ property: {
+ type: 'Identifier',
+ name: 'isArray',
+ },
+ computed: false,
+ },
+ arguments: [
+ {
+ type: 'UnaryExpression',
+ operator: '',
+ prefix: false,
+ argument: ctx.ITERABLE,
+ },
+ ],
+ },
+ },
+ consequent: {
+ type: 'UnaryExpression',
+ operator: '',
+ prefix: false,
+ argument: ctx.ITERABLE,
+ },
+ alternate: {
+ type: 'CallExpression',
+ callee: {
+ type: 'ArrowFunctionExpression',
+ id: null,
+ generator: false,
+ expression: false,
+ async: false,
+ params: [],
+ body: {
+ type: 'BlockStatement',
+ body: [
+ {
+ type: 'ThrowStatement',
+ argument: {
+ type: 'NewExpression',
+ callee: {
+ type: 'Identifier',
+ name: 'Error',
+ },
+ arguments: [
+ {
+ type:
+ 'BinaryExpression',
+ left: {
+ type:
+ 'BinaryExpression',
+ left: {
+ type:
+ 'StringLiteral',
+ extra: {
+ rawValue:
+ 'Dynamic attributes have to be an array, found ',
+ raw:
+ '"Dynamic attributes have to be an array, found "',
+ },
+ value:
+ 'Dynamic attributes have to be an array, found ',
+ },
+ operator: '+',
+ right: {
+ type:
+ 'UnaryExpression',
+ operator:
+ 'typeof',
+ prefix: true,
+ argument:
+ ctx.ITERABLE,
+ },
+ },
+ operator: '+',
+ right: {
+ type:
+ 'StringLiteral',
+ extra: {
+ rawValue:
+ ' instead',
+ raw:
+ '" instead"',
+ },
+ value: ' instead',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ directives: [],
+ },
+ },
+ arguments: [],
+ },
+ },
+ },
+ {
+ type: 'VariableDeclarator',
+ id: ctx.LENGTH,
+ init: {
+ type: 'MemberExpression',
+ object: ctx.LOCAL_ITERABLE,
+ property: {
+ type: 'Identifier',
+ name: 'length',
+ },
+ computed: false,
+ },
+ },
+ ],
+ kind: 'let',
+ },
+ test: {
+ type: 'BinaryExpression',
+ left: ctx.INDEX,
+ operator: '<',
+ right: ctx.LENGTH,
+ },
+ update: {
+ type: 'AssignmentExpression',
+ operator: '+=',
+ left: ctx.INDEX,
+ right: {
+ type: 'NumericLiteral',
+ extra: {
+ rawValue: 2,
+ raw: '2',
+ },
+ value: 2,
+ },
+ },
+ body: {
+ type: 'BlockStatement',
+ body: [
+ {
+ type: 'ExpressionStatement',
+ expression: {
+ type: 'CallExpression',
+ callee: ctx.ATTR,
+ arguments: [
+ {
+ type: 'MemberExpression',
+ object: ctx.LOCAL_ITERABLE,
+ property: ctx.INDEX,
+ computed: true,
+ },
+ {
+ type: 'MemberExpression',
+ object: ctx.LOCAL_ITERABLE,
+ property: {
+ type: 'BinaryExpression',
+ left: ctx.INDEX,
+ operator: '+',
+ right: {
+ type: 'NumericLiteral',
+ extra: {
+ rawValue: 1,
+ raw: '1',
+ },
+ value: 1,
+ },
+ },
+ computed: true,
+ },
+ ],
+ },
+ },
+ ],
+ directives: [],
+ },
+ };
+}
diff --git a/packages/melody-plugin-idom/src/visitors/mount.js b/packages/melody-plugin-idom/src/visitors/mount.js
new file mode 100644
index 0000000..a2cb75a
--- /dev/null
+++ b/packages/melody-plugin-idom/src/visitors/mount.js
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+
+function parentIsForStatement(path) {
+ const { parentPath } = path;
+
+ if (!parentPath) {
+ return false;
+ }
+
+ if (parentPath.is('ForStatement')) {
+ return true;
+ }
+
+ if (parentPath.is('Element')) {
+ return false;
+ }
+
+ return parentIsForStatement(parentPath);
+}
+
+export default {
+ convert: {
+ MountStatement: {
+ exit(path) {
+ const args = [];
+ if (path.node.source) {
+ const source = path.node.source.value;
+ let localName;
+ if (path.node.name) {
+ localName = this.addImportFrom(
+ source,
+ path.node.name.name,
+ );
+ } else {
+ localName = this.addDefaultImportFrom(
+ source,
+ this.generateComponentUid(source),
+ );
+ }
+ path.scope.registerBinding(localName, path, 'var');
+ this.markIdentifier(localName);
+ args.push(t.identifier(localName));
+ } else {
+ args.push(path.node.name);
+ }
+ if (path.node.key) {
+ if (path.get('key').is('StringLiteral')) {
+ args.push(path.node.key);
+ } else {
+ args.push(
+ t.binaryExpression(
+ '+',
+ t.stringLiteral(''),
+ path.node.key,
+ ),
+ );
+ }
+ } else if (
+ !parentIsForStatement(path) &&
+ this.options.generateKey
+ ) {
+ args.push(t.stringLiteral(this.generateKey()));
+ } else {
+ args.push(t.nullLiteral());
+ }
+ if (path.node.argument) {
+ args.push(path.node.argument);
+ }
+
+ path.replaceWithJS(
+ t.expressionStatement(
+ t.callExpression(
+ t.identifier(
+ path.state.addImportFrom(
+ 'melody-idom',
+ 'component',
+ ),
+ ),
+ args,
+ ),
+ ),
+ );
+ },
+ },
+ },
+};
diff --git a/packages/melody-plugin-jsx/__tests__/CompilerSpec.js b/packages/melody-plugin-jsx/__tests__/CompilerSpec.js
new file mode 100644
index 0000000..6bfaa13
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/CompilerSpec.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { compile, toString } from 'melody-compiler';
+import { extension as coreExtension } from 'melody-extension-core';
+import jsxPlugin from '../src';
+import fs from 'fs';
+import path from 'path';
+
+describe('Compiler', function() {
+ getFixtures('success').forEach(({ name, twigPath }) => {
+ it('should correctly transform ' + name.replace(/_/g, ' '), function() {
+ fixture(twigPath, name);
+ });
+ });
+
+ getFixtures('error').forEach(({ name, twigPath }) => {
+ it('should fail transforming ' + name.replace(/_/g, ' '), function() {
+ expect(
+ fixture.bind(null, twigPath, name),
+ ).toThrowErrorMatchingSnapshot();
+ });
+ });
+});
+
+function getFixtures(type) {
+ const dirPath = path.join(__dirname, '__fixtures__', type);
+ return fs.readdirSync(dirPath).map(name => ({
+ name: path.basename(name, '.twig'),
+ twigPath: path.join(dirPath, name),
+ }));
+}
+
+function fixture(twigPath, name) {
+ const twig = fs.readFileSync(twigPath).toString();
+
+ const jsAst = compile(name + '.twig', twig, coreExtension, jsxPlugin);
+ const actual = toString(jsAst, twig).code;
+
+ expect(`\n${actual}\n`).toMatchSnapshot();
+}
diff --git a/packages/melody-plugin-jsx/__tests__/JSXSpec.js b/packages/melody-plugin-jsx/__tests__/JSXSpec.js
new file mode 100644
index 0000000..3ed500f
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/JSXSpec.js
@@ -0,0 +1,146 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { compile, toString } from 'melody-compiler';
+import { extension as coreExtension } from 'melody-extension-core';
+import jsxPlugin from '../src';
+import { stripIndent } from 'common-tags';
+
+function getOutput(code) {
+ return toString(compile('test.twig', code, coreExtension, jsxPlugin), code)
+ .code;
+}
+
+describe('JSX', function() {
+ it('should remove attributes filters', function() {
+ const code = stripIndent`
+
+ `;
+ expect(getOutput(code)).toMatchSnapshot();
+ });
+ it('should support object expression', function() {
+ const code = stripIndent`
+
+ `;
+ expect(getOutput(code)).toMatchSnapshot();
+ });
+ it('should spread identifiers', function() {
+ const code = stripIndent`
+
+ `;
+ expect(getOutput(code)).toMatchSnapshot();
+ });
+ it('should transform string literals', function() {
+ const code = stripIndent`
+
+ `;
+ expect(getOutput(code)).toMatchSnapshot();
+ });
+ it('should transform attributes to jsx naming conventions', function() {
+ const code = stripIndent`
+
+ `;
+ expect(getOutput(code)).toMatchSnapshot();
+ });
+ it('should hoist attributes that will get mutated', function() {
+ const code = stripIndent`
+ {% set foo = 'bar' %}
+
+ {% set foo = 'qux' %}
+
Hello World
+
+ `;
+ expect(getOutput(code)).toMatchSnapshot();
+ });
+ it('should hoist optimized attributes that will get mutated', function() {
+ const code = stripIndent`
+ {% set foo = 'bar' %}
+
+ {% set foo = 'qux' %}
+
Hello World
+
+ `;
+ expect(getOutput(code)).toMatchSnapshot();
+ });
+ it('should work with loops', function() {
+ const code = stripIndent`
+
+ {% for item in items %}
+ {% set className = 'li-' ~ id %}
+
+ {% set className = 'span-' ~ id %}
+ {{ item.label }}
+
+ {% endfor %}
+
+ `;
+ expect(getOutput(code)).toMatchSnapshot();
+ });
+ it('should work with loops and multiple roots', function() {
+ const code = stripIndent`
+
+ {% for item in items %}
+
+ Hello World
+
+ Hotel? Trivago!
+ {% if foo %}
+ Foo you!
+ {% endif %}
+ {% endfor %}
+
+ `;
+ expect(getOutput(code)).toMatchSnapshot();
+ });
+ it('should work with complex conditions', function() {
+ const code = stripIndent`
+ {% set foo = 'bar' %}
+
+ {% set foo = 'qux' %}
+
Hello World
+ {% if bar == 'foo' %}
+ {% set foo = 'nom' %}
+
Hello
+ {% elseif bar == 'nom' %}
+
+ {% if qux == 'nom' %}
+
Qux
+ {% endif %}
+ {% else %}
+
Look at this {{ bar }}
+ {% endif %}
+
+ `;
+ expect(getOutput(code)).toMatchSnapshot();
+ });
+});
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/error/unknown_filter.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/error/unknown_filter.template
new file mode 100644
index 0000000..c9d140b
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/error/unknown_filter.template
@@ -0,0 +1 @@
+{{ test | unknown }}
\ No newline at end of file
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/block.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/block.template
new file mode 100644
index 0000000..0a7db1a
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/block.template
@@ -0,0 +1,15 @@
+{% use "foo.twig" %}
+{% use "blocks.html" with sidebar as base_sidebar, title as base_title %}
+
+ {% block hello %}
+
+ {{ message | lower | upper }}{% flush %}
+ {{ _context.name[1:] }}
+ {{ block('test') }}
+ {{ include('test.twig') }}
+ {% include 'test.twig' %}
+
+ {% endblock %}
+ {% block bar foo %}
+ {{ block('hello') }}
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/dynamicAttribute.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/dynamicAttribute.template
new file mode 100644
index 0000000..ffc27e0
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/dynamicAttribute.template
@@ -0,0 +1,3 @@
+
+ Answer
+
\ No newline at end of file
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/embed_nested.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/embed_nested.template
new file mode 100644
index 0000000..380ca94
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/embed_nested.template
@@ -0,0 +1,20 @@
+{% extends "parent.twig" %}
+
+{% block hello %}
+
+ {% embed "foo.twig" with { foo: 'bar' } %}
+ {% block hello %}
+
+ {{ fun }}
+ {% embed "bar.twig" %}
+ {% block hello %}
+ {{ message }}
+ {% endblock %}
+ {% block test %}
+ {% endblock %}
+ {% endembed %}
+
+ {% endblock hello %}
+ {% endembed %}
+
+{% endblock %}
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/extends.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/extends.template
new file mode 100644
index 0000000..edf65e3
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/extends.template
@@ -0,0 +1,13 @@
+{% extends "parent.twig" %}
+
+{% block hello %}
+
+ {{ message }}&
+ {% include "test.twig" with { foo: "bar" } only %}
+ {{ include("test.twig", { foo: "bar" }) }}
+
+ {% mount Item with item %}
+ {% mount './Item' with item %}
+ {% mount Item from './ItemList' with {item: item} %}
+
+{% endblock %}
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/extends_with_context.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/extends_with_context.template
new file mode 100644
index 0000000..eb54b35
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/extends_with_context.template
@@ -0,0 +1,8 @@
+{% set foo = "bar" %}
+{% extends "parent.twig" %}
+
+{% block hello %}
+
+ {{ message }}
+
+{% endblock %}
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_if_else.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_if_else.template
new file mode 100644
index 0000000..9b29eb5
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_if_else.template
@@ -0,0 +1,19 @@
+
+
+ {% for a,b in c | slice(3, c.length) if b is even %}
+ {{ a }} - {{ b }}
+ {% else %}
+ No results found
+ {% endfor %}
+
+
+
+ {% for a,b in c[:c.length - 1] if b is defined and not b is even %}
+ {{ a }} - {{ b }}
+ {% else %}
+ {% if regionName is empty %}
+ No results found
+ {% endif %}
+ {% endfor %}
+
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_local.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_local.template
new file mode 100644
index 0000000..eb82cc5
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_local.template
@@ -0,0 +1,7 @@
+
+ {% for item in items %}
+
+ {{ loop.index0 // 2 }} {{ item.name }} {{ loop.index }}
+
+ {% endfor %}
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_block.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_block.template
new file mode 100644
index 0000000..ecc315f
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_block.template
@@ -0,0 +1,12 @@
+
+
{{ title | title }}
+
+ {% for item in items %}
+
+ {% block title %}
+ {{ loop.index0 }} {{ item.name | title }} {{ loop.index }}
+ {% endblock %}
+
+ {% endfor %}
+
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_block_and_key.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_block_and_key.template
new file mode 100644
index 0000000..3e8911f
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_block_and_key.template
@@ -0,0 +1,9 @@
+
+ {% for i, item in items %}
+
+ {% block itemContent %}
+ {{i}} {{ loop.index0 }} {{ item.name }} {{ loop.index }}
+ {% endblock %}
+
+ {% endfor %}
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_include.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_include.template
new file mode 100644
index 0000000..d7755c6
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_include.template
@@ -0,0 +1,7 @@
+
+ {% for foo in range(1, category) %}
+
+ {% include './Star.twig' %}
+
+ {% endfor %}
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_include_only.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_include_only.template
new file mode 100644
index 0000000..95e069a
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/for_with_include_only.template
@@ -0,0 +1,7 @@
+
+ {% for foo in range(1, category) %}
+
+ {% include './Star.twig' only %}
+
+ {% endfor %}
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/itemElement.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/itemElement.template
new file mode 100644
index 0000000..b934761
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/itemElement.template
@@ -0,0 +1,465 @@
+
+ Deals
+
+
+
+
+
+ No image for this deal.
+
+
+
+ Add the Lorem Ipsum dolor sit amet to my favorites list
+
+
+
+
+
+
+
+ Lorem Ipsum dolor sit amet
+
+
+
Next to location:
+
+
+ Amsterdam,
+ 1.2km to City centre
+
+
+
+
+
+
“Good”
+
+
+
+ 76 / 100 -
+ hotel rating (754 reviews)
+
+
+
+
+
+
+
+
+ Other deals
+
+ View more deals: 17
+
+
+
+
+
Best of 17 websites
+
+
+
+
+
+
+
Change date to see available deals.
+
+
+
+
+
+
+
+
+
+
+
+ Detailed information for this deal
+
+
+ Navigation for detailed information
+
+ Close
+
+
+
+
+
Slideout contents here
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
+
+
+
+
+
+
+
+ No image for this deal.
+
+
+
+ Add the Lorem Ipsum dolor sit amet to my favorites list
+
+
+
+
+
+
+
+ Lorem Ipsum dolor sit amet
+
+
+
Next to location:
+
+
+ Amsterdam,
+ 0.9km to London Heathrow Airport
+
+
+
+
+
+
“Good”
+
+
+
+ 76 / 100 -
+ hotel rating (754 reviews)
+
+
+
+
+
+
+
+
+ Other deals
+
+ View more deals: 17
+
+
+
+
+
Best of 17 websites
+
+
+
+
+
+
+
Change date to see available deals.
+
+
+
+
+
+
+
+
+
+
+
+ Detailed information for this deal
+
+
+ Navigation for detailed information
+
+ Close
+
+
+
+
+
Slideout contents here
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
+
+
+
+
+
+
+
+ No image for this deal.
+
+
+
+ Add the Lorem Ipsum dolor sit amet to my favorites list
+
+
+
+
+
+
+
+ Lorem Ipsum dolor sit amet
+
+
+
Next to location:
+
+
+ Amsterdam,
+ 1.5km to International Airport Amsterdam
+
+
+
+
+
+
“Good”
+
+
+
+ 76 / 100 -
+ hotel rating (754 reviews)
+
+
+
+
+
+
+
+
+ Other deals
+
+ View more deals: 17
+
+
+
+
+
Best of 17 websites
+
+
+
Breakfast included
+
+
+
+
Change date to see available deals.
+
+
+
+
+
+
+
+
+
+
+
+ Detailed information for this deal
+
+
+ Navigation for detailed information
+
+ Close
+
+
+
+
+
Slideout contents here
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
+
+
+
+
+
+
+
+ No image for this deal.
+
+
+
+ Add the Lorem Ipsum dolor sit amet to my favorites list
+
+
+
+
+
+
+
+ Lorem Ipsum dolor sit amet
+
+
+
Next to location:
+
+
+ Amsterdam,
+ 0.5km to Oxford Street
+
+
+
+
+
+
“Good”
+
+
+
+ 76 / 100 -
+ hotel rating (754 reviews)
+
+
+
+
+
+
+
+
+ Other deals
+
+ View more deals: 17
+
+
+
+
+
Best of 17 websites
+
+
+
+
+
+
+
Change date to see available deals.
+
+
+
+
+
+
+
+
+
+
+
+ Detailed information for this deal
+
+
+ Navigation for detailed information
+
+ Close
+
+
+
+
+
Slideout contents here
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
+
+
+
+
+
+
+
+ No image for this deal.
+
+
+
+ Add the Lorem Ipsum dolor sit amet to my favorites list
+
+
+
+
+
+
+
+ Lorem Ipsum dolor sit amet
+
+
+
Next to location:
+
+
+ Amsterdam,
+ 0.8km to Alexanderplatz
+
+
+
+
+
+
“Good”
+
+
+
+ 76 / 100 -
+ hotel rating (754 reviews)
+
+
+
+
+
+
+
+
+ Other deals
+
+ View more deals: 17
+
+
+
+
+
Best of 17 websites
+
+
+
+
+
+
+
Change date to see available deals.
+
+
+
+
+
+
+
+
+
+
+
+ Detailed information for this deal
+
+
+ Navigation for detailed information
+
+ Close
+
+
+
+
+
Slideout contents here
+
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
+
+
+
+
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/macros.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/macros.template
new file mode 100644
index 0000000..404bc8d
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/macros.template
@@ -0,0 +1,24 @@
+{% macro input(name, value, type, size) %}
+
+{% endmacro %}
+
+{% import "forms.html" as foo %}
+{% import _self as forms %}
+{% from 'forms.html' import input as input_field %}
+
+
+ {{ foo.bar('test') }}
+ {{ forms.input('foo', 'bar', 'baz', 42) }}
+ {{ input_field('foo') }}
+
+ {% block test %}
+ {% import "forms.html" as foo %}
+ {% import _self as forms %}
+ {% from 'forms.html' import input as input_field %}
+
+ {{ foo.bar('test') }}
+ {{ forms.input('foo', 'bar', 'baz', 42) }}
+ {{ input_field('foo') }}
+
+ {% endblock %}
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/mount.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/mount.template
new file mode 100644
index 0000000..f7d4a55
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/mount.template
@@ -0,0 +1,10 @@
+
+ {% mount './component' %}
+ {% mount 'foo.twig' as 'bar' %}
+ {% mount 'foo.twig' as 'bar' with {foo: 'bar'} %}
+ {% mount 'foo.twig' with {foo: 'bar'} %}
+ {% mount Foo %}
+ {% mount Foo as bar %}
+ {% mount Foo as 'bar' with {foo: 'bar'} %}
+ {% mount Foo with {foo: 'bar'} %}
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/multi_include.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/multi_include.template
new file mode 100644
index 0000000..4715d22
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/multi_include.template
@@ -0,0 +1,125 @@
+{% set tEB_SAME_WINDOW_CLICKOUT = false %} {# ExpressBooking #}
+{% set ironItem1p1 = true %} {# #}
+{% set bClickoutClientside = false %}
+{% set sBtnModifiers = '' %}
+{% spaceless %}
+
+
+
+
+ {# if 'WEB-28840' is not active #}
+ {% if not ironItem1p1 %}
+ {% if rateCount is defined %}
+
{{ 'ie_bestdeal' }}
+ {% endif %}
+ {% endif %}
+
+ {% if bestPriceDeal %}
+ {% set flagModifiers = "trv-maincolor-03 font-bright cur-pointer--hover" %}
+ {% if ironItem1p1 %}
+ {% set flagModifiers = "font-bright cur-pointer--hover" %}
+ {% endif %}
+ {% if isTopDeal %}
+ {% set dataVariables = {
+ "data-topdeal-percentage": topDealPercentage,
+ "data-topdeal-price": bestPriceDeal.price,
+ "data-topdeal-compare-price": topDealComparePrice,
+ "data-topdeal-criterion": topDealCriterion,
+ "data-topdeal-criterion-id": topDealCriterionId,
+ "data-topdeal-category": category,
+ "data-topdeal-path-name": pathName,
+ "data-topdeal-overall-liking": overallLiking
+ } %}
+ {% set flagModifiers = flagModifiers ~ " top_deals js_top_deals " ~ topDealCriterion %}
+ {% set dataVariables = dataVariables | merge({'data-topdeal-log': '1'}) %}
+ {% include "./Flag.twig" with {
+ "styleModifier": flagModifiers,
+ "dataVariables": dataVariables,
+ "text": "ie_topdeal"
+ } only %}
+ {% else %}
+ {% if iSaving >= 20 %}
+ {% include "./Flag.twig" with {
+ "styleModifier": flagModifiers,
+ "dataVariables": dataVariables,
+ "text": "-" ~ saving ~ "%"
+ } only %}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+
+
+
+
+ {# New position of advertiser string depending on WEB-29007 #}
+ {% if false %}
+
+ {% if bestPriceDeal.groupId == 80 and bestPriceDeal.useLocalizedHotelWebsiteLogo %}
+ {{ 'book_hotel_website_test' }}
+ {% else %}
+ {{ bestPriceDeal.sName }}
+ {% endif %}
+
+ {% endif %}
+
+ {% if bestPriceDeal %}
+ {% if maxPriceDeal %}
+
+
+
{{ maxPriceDeal.price }}
+ {% else %}
+
+ {% endif %}
+
{{ bestPriceDeal.price }}
+
+ {# Old/regular position of advertiser string without WEB-29007 #}
+ {% if false %}
+
+ {% if oBestPriceDeal.iGroupId == 80 and oBestPriceDeal.bUseLocalizedHotelWebsiteLogo %}
+ {{ 'book_hotel_website_test' }}
+ {% else %}
+ {{ oBestPriceDeal.sName }}
+ {% endif %}
+
+ {% endif %}
+
+ {% if true %}
+ {% set sBtnModifiers = "btn--deal fl-trailing" %}
+ {% endif %}
+ {% if false %}
+ {% set sBtnModifiers = sBtnModifiers ~ " alt" %}
+ {% endif %}
+ {% elseif state == 1 %}
+
{{ 'unavailable_deal' }}
+
+ {% if true %}
+ {% set sBtnModifiers = "btn--icon btn--deal btn--disabled fl-trailing trv-maincolor-04-very-light" %}
+ {% endif %}
+ {% else %}
+
+ {% if true %}
+ {% set sBtnModifiers = "btn--icon btn--deal btn--disabled fl-trailing trv-maincolor-04-very-light" %}
+ {% endif %}
+ {% endif %}
+
+
+
+ {% include "./Button.html.twig" with { "styleModifier": sBtnModifiers, "text": "deals_forward_new" } only %}
+
+
+{% endspaceless %}
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/raw.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/raw.template
new file mode 100644
index 0000000..2934577
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/raw.template
@@ -0,0 +1,9 @@
+
+ {% set vars = {
+ "foo": 'foo ',
+ "bar": 'bar ' | raw
+ } %}
+
+ {{ vars.foo | raw }}
+ {{ vars.bar }}
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/ref.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/ref.template
new file mode 100644
index 0000000..6a60b0d
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/ref.template
@@ -0,0 +1,7 @@
+
+ udisuabcd
+
+
+
+ test
+
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/spaceless.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/spaceless.template
new file mode 100644
index 0000000..34c6bff
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/spaceless.template
@@ -0,0 +1,6 @@
+{% spaceless %}
+
+ Receive {{ formattedIncentive }} cash back for testing this hotel.
+ Or just be happy!
+
+{% endspaceless %}
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/styles.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/styles.template
new file mode 100644
index 0000000..c313767
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/styles.template
@@ -0,0 +1,30 @@
+Lorem Ipsum
+
+Lorem Ipsum
+
+Lorem Ipsum
+
+Lorem Ipsum
+
+Lorem Ipsum
+
+Lorem Ipsum
diff --git a/packages/melody-plugin-jsx/__tests__/__fixtures__/success/svg.template b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/svg.template
new file mode 100644
index 0000000..a8c7a6a
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__fixtures__/success/svg.template
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/melody-plugin-jsx/__tests__/__snapshots__/CompilerSpec.js.snap b/packages/melody-plugin-jsx/__tests__/__snapshots__/CompilerSpec.js.snap
new file mode 100644
index 0000000..ee963e0
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__snapshots__/CompilerSpec.js.snap
@@ -0,0 +1,883 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Compiler should correctly transform block.template 1`] = `
+"
+import _include from \\"test.twig\\";
+import { renderBase_sidebar as renderSidebar, renderBase_title as renderTitle } from \\"blocks.html\\";
+import { inheritBlocks } from \\"melody-runtime\\";
+import _use from \\"foo.twig\\";
+export const _template = {};
+inheritBlocks(_template, _use);
+Object.assign(_template, {
+ renderBase_sidebar,
+ renderBase_title
+});
+
+_template.renderHello = function (_context) {
+ return {_context.message.toLowerCase().toUpperCase()}{_context.name.slice(1, _context.name.length)} {this.renderTest(_context)}{_include.render()}{_include(_context)}
;
+};
+
+_template.renderBar = function (_context) {
+ return _context.foo;
+};
+
+_template.render = function (_context) {
+ return {this.renderHello(_context)}{this.renderBar(_context)}{this.renderHello(_context)} ;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Block\\";
+}
+
+export default function Block(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform dynamicAttribute.template 1`] = `
+"
+export const _template = {};
+
+_template.render = function (_context) {
+ return Answer ;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"DynamicAttribute\\";
+}
+
+export default function DynamicAttribute(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform embed nested.template 1`] = `
+"
+import { _template as _parent } from \\"parent.twig\\";
+import { createSubContext } from \\"melody-runtime\\";
+import { _template as _embed$2 } from \\"foo.twig\\";
+import { _template as _embed } from \\"bar.twig\\";
+export const _template = Object.create(_parent);
+
+const _embed$1 = Object.create(_embed),
+ _embed$3 = Object.create(_embed$2);
+
+_embed$3.renderHello = function (_context) {
+ return {_context.fun}{_embed$1.render(_context)}
;
+};
+
+_embed$1.renderHello = function (_context) {
+ return _context.message;
+};
+
+_embed$1.renderTest = function (_context) {};
+
+_template.renderHello = function (_context) {
+ return {_embed$3.render(createSubContext(_context, {
+ foo: \\"bar\\"
+ }))}
;
+};
+
+_template.render = function (_context) {
+ return _parent.render.call(_template, _context);
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"EmbedNested\\";
+}
+
+export default function EmbedNested(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform extends with context.template 1`] = `
+"
+import { _template as _parent } from \\"parent.twig\\";
+import { createSubContext } from \\"melody-runtime\\";
+export const _template = Object.create(_parent);
+
+_template.renderHello = function (_context) {
+ return {_context.message}
;
+};
+
+_template.render = function (_context) {
+ const _context$1 = createSubContext(_context);
+
+ _context$1.foo = \\"bar\\";
+ return _parent.render.call(_template, _context$1);
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"ExtendsWithContext\\";
+}
+
+export default function ExtendsWithContext(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform extends.template 1`] = `
+"
+import { _template as _parent } from \\"parent.twig\\";
+import { Item as Item$2 } from \\"./ItemList\\";
+import Item$1 from \\"./Item\\";
+import _include$1 from \\"test.twig\\";
+import _include from \\"test.twig\\";
+export const _template = Object.create(_parent);
+
+_template.renderHello = function (_context) {
+ return {_context.message}&{_include({
+ foo: \\"bar\\"
+ })}{_include$1.render({
+ foo: \\"bar\\"
+ })}<_context.Item {..._context.item} />
;
+};
+
+_template.render = function (_context) {
+ return _parent.render.call(_template, _context);
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Extends\\";
+}
+
+export default function Extends(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform for if else.template 1`] = `
+"
+import { isEmpty } from \\"melody-runtime\\";
+export const _template = {};
+
+_template.render = function (_context) {
+ let _child, _child$2;
+
+ let _child$1 = new Array();
+
+ let _uniterated = true;
+ {
+ let _sequence = _context.c.slice(3, _context.c.length),
+ a = 0,
+ _length = _sequence.length,
+ b = _sequence[0];
+
+ for (; a < _length; a++, b = _sequence[_index]) {
+ if (!(b % 2)) {
+ _child$1[_child$1.length] = {a} - {b} ;
+ _uniterated = false;
+ }
+ }
+ }
+
+ if (_uniterated) {
+ _child$1[_child$1.length] = No results found ;
+ }
+
+ _child = ;
+
+ let _child$3 = new Array();
+
+ let _uniterated$1 = true;
+ {
+ let _sequence = _context.c.slice(0, _context.c.length - 1),
+ a = 0,
+ _length = _sequence.length,
+ b = _sequence[0];
+
+ for (; a < _length; a++, b = _sequence[_index]) {
+ if (typeof b !== \\"undefined\\" && !!(b % 2)) {
+ _child$3[_child$3.length] = {a} - {b} ;
+ _uniterated$1 = false;
+ }
+ }
+ }
+
+ if (_uniterated$1) {
+ if (isEmpty(_context.regionName)) {
+ _child$3[_child$3.length] = No results found ;
+ }
+ }
+
+ _child$2 = ;
+ return {_child}{_child$2}
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"ForIfElse\\";
+}
+
+export default function ForIfElse(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform for local.template 1`] = `
+"
+import { round } from \\"melody-runtime\\";
+export const _template = {};
+
+_template.render = function (_context) {
+ let _child = new Array();
+
+ {
+ let _sequence = _context.items,
+ _index = 0,
+ _length = _sequence.length,
+ item = _sequence[0],
+ _index$1 = 1,
+ _revindex = _length,
+ _revindex$1 = _length - 1,
+ _first = true,
+ _last = 1 === _length;
+
+ for (; _index < _length; _index++, item = _sequence[_index]) {
+ _child[_child.length] = {round((_index$1 - 1) / 2, 0, \\"floor\\")}{item.name}{_index$1} ;
+ _index$1++;
+ _revindex--;
+ _revindex$1--;
+ _first = false;
+ _last = _revindex$1 === 0;
+ }
+ }
+ return ;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"ForLocal\\";
+}
+
+export default function ForLocal(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform for with block and key.template 1`] = `
+"
+import { createSubContext } from \\"melody-runtime\\";
+export const _template = {};
+
+_template.renderItemContent = function (_context) {
+ return {_context.i}{_context.loop.index0}{_context.item.name}{_context.loop.index} ;
+};
+
+_template.render = function (_context) {
+ let _child = new Array();
+
+ {
+ let _sequence = _context.items,
+ i = 0,
+ _length = _sequence.length,
+ _context$1 = createSubContext(_context, {
+ item: _sequence[0],
+ loop: {
+ index: 1,
+ index0: 0,
+ length: _length,
+ revindex: _length,
+ revindex0: _length - 1,
+ first: true,
+ last: 1 === _length
+ },
+ i: i
+ });
+
+ for (; i < _length; i++) {
+ _child[_child.length] = {this.renderItemContent(_context$1)} ;
+ _context$1.loop.index0++;
+ _context$1.loop.index++;
+ _context$1.loop.revindex--;
+ _context$1.loop.revindex0--;
+ _context$1.loop.first = false;
+ _context$1.loop.last = _context$1.loop.revindex === 0;
+ _context$1.item = _sequence[i + 1];
+ _context$1.i = i;
+ }
+ }
+ return ;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"ForWithBlockAndKey\\";
+}
+
+export default function ForWithBlockAndKey(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform for with block.template 1`] = `
+"
+import { title, createSubContext } from \\"melody-runtime\\";
+export const _template = {};
+
+_template.renderTitle = function (_context) {
+ return _context.loop.index0;
+ return title(_context.item.name);
+ return _context.loop.index;
+};
+
+_template.render = function (_context) {
+ let _child, _child$2;
+
+ _child = {title(_context.title)} ;
+
+ let _child$3 = new Array();
+
+ {
+ let _sequence = _context.items,
+ _index = 0,
+ _length = _sequence.length,
+ _context$1 = createSubContext(_context, {
+ item: _sequence[0],
+ loop: {
+ index: 1,
+ index0: 0,
+ length: _length,
+ revindex: _length,
+ revindex0: _length - 1,
+ first: true,
+ last: 1 === _length
+ }
+ });
+
+ for (; _index < _length; _index++) {
+ _child$3[_child$3.length] = {this.renderTitle(_context$1)} ;
+ _context$1.loop.index0++;
+ _context$1.loop.index++;
+ _context$1.loop.revindex--;
+ _context$1.loop.revindex0--;
+ _context$1.loop.first = false;
+ _context$1.loop.last = _context$1.loop.revindex === 0;
+ _context$1.item = _sequence[_index + 1];
+ }
+ }
+ _child$2 = ;
+ return {_child}{_child$2}
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"ForWithBlock\\";
+}
+
+export default function ForWithBlock(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform for with include only.template 1`] = `
+"
+import _include from \\"./Star.twig\\";
+import { range as range$1 } from \\"lodash\\";
+export const _template = {};
+
+_template.render = function (_context) {
+ let _child = new Array();
+
+ {
+ let _sequence = range$1(1, _context.category + 1),
+ _index = 0,
+ _length = _sequence.length,
+ foo = _sequence[0];
+
+ for (; _index < _length; _index++, foo = _sequence[_index]) {
+ _child[_child.length] = {_include()} ;
+ }
+ }
+ return {_child}
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"ForWithIncludeOnly\\";
+}
+
+export default function ForWithIncludeOnly(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform for with include.template 1`] = `
+"
+import { createSubContext } from \\"melody-runtime\\";
+import _include from \\"./Star.twig\\";
+import { range as range$1 } from \\"lodash\\";
+export const _template = {};
+
+_template.render = function (_context) {
+ let _child = new Array();
+
+ {
+ let _sequence = range$1(1, _context.category + 1),
+ _index = 0,
+ _length = _sequence.length,
+ _context$1 = createSubContext(_context, {
+ foo: _sequence[0],
+ loop: {
+ index: 1,
+ index0: 0,
+ length: _length,
+ revindex: _length,
+ revindex0: _length - 1,
+ first: true,
+ last: 1 === _length
+ }
+ });
+
+ for (; _index < _length; _index++) {
+ _child[_child.length] = {_include(_context$1)} ;
+ _context$1.loop.index0++;
+ _context$1.loop.index++;
+ _context$1.loop.revindex--;
+ _context$1.loop.revindex0--;
+ _context$1.loop.first = false;
+ _context$1.loop.last = _context$1.loop.revindex === 0;
+ _context$1.foo = _sequence[_index + 1];
+ }
+ }
+ return {_child}
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"ForWithInclude\\";
+}
+
+export default function ForWithInclude(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform itemElement.template 1`] = `
+"
+export const _template = {};
+
+_template.render = function (_context) {
+ return Deals No image for this deal. Add the Lorem Ipsum dolor sit amet to my favorites list
Lorem Ipsum dolor sit amet Next to location: Amsterdam, 1.2km to City centre
“Good” 76 / 100 - hotel rating (754 reviews)
Other deals View more deals: 17 Best of 17 websites Change date to see available deals.
Detailed information for this deal Navigation for detailed information Close
Slideout contents here Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
No image for this deal. Add the Lorem Ipsum dolor sit amet to my favorites list
Lorem Ipsum dolor sit amet Next to location: Amsterdam, 0.9km to London Heathrow Airport
“Good” 76 / 100 - hotel rating (754 reviews)
Other deals View more deals: 17 Best of 17 websites Change date to see available deals.
Detailed information for this deal Navigation for detailed information Close
Slideout contents here Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
No image for this deal. Add the Lorem Ipsum dolor sit amet to my favorites list
Lorem Ipsum dolor sit amet Next to location: Amsterdam, 1.5km to International Airport Amsterdam
“Good” 76 / 100 - hotel rating (754 reviews)
Other deals View more deals: 17 Best of 17 websites Breakfast included Change date to see available deals.
Detailed information for this deal Navigation for detailed information Close
Slideout contents here Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
No image for this deal. Add the Lorem Ipsum dolor sit amet to my favorites list
Lorem Ipsum dolor sit amet Next to location: Amsterdam, 0.5km to Oxford Street
“Good” 76 / 100 - hotel rating (754 reviews)
Other deals View more deals: 17 Best of 17 websites Change date to see available deals.
Detailed information for this deal Navigation for detailed information Close
Slideout contents here Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
No image for this deal. Add the Lorem Ipsum dolor sit amet to my favorites list
Lorem Ipsum dolor sit amet Next to location: Amsterdam, 0.8km to Alexanderplatz
“Good” 76 / 100 - hotel rating (754 reviews)
Other deals View more deals: 17 Best of 17 websites Change date to see available deals.
Detailed information for this deal Navigation for detailed information Close
Slideout contents here Lorem ipsum dolor sit amet, consectetur adipisicing elit. Error et eveniet hic numquam quisquam vel vitae!
+ Atque consectetur consequatur, dignissimos dolor eius harum officiis pariatur qui quia reprehenderit sit
+ ullam veritatis! Amet error exercitationem ipsam quibusdam ullam! Aperiam aspernatur aut harum hic impedit
+ sequi sint tenetur ullam velit, vitae voluptatibus.
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"ItemElement\\";
+}
+
+export default function ItemElement(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform macros.template 1`] = `
+"
+import { input as input_field } from \\"forms.html\\";
+import * as foo from \\"forms.html\\";
+export const _template = {};
+export function input(name, value, type, size, ...varargs) {
+ return ;
+}
+
+_template.renderTest = function (_context) {
+ const forms = {
+ input
+ };
+ return {foo.bar(\\"test\\")}{forms.input(\\"foo\\", \\"bar\\", \\"baz\\", 42)}{input_field(\\"foo\\")}
;
+};
+
+_template.render = function (_context) {
+ const forms = {
+ input
+ };
+ return {foo.bar(\\"test\\")}{forms.input(\\"foo\\", \\"bar\\", \\"baz\\", 42)}{input_field(\\"foo\\")}{this.renderTest(_context)}
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Macros\\";
+}
+
+export default function Macros(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform mount.template 1`] = `
+"
+import Footwig from \\"foo.twig\\";
+import Component from \\"./component\\";
+export const _template = {};
+
+_template.render = function (_context) {
+ return <_context.Foo /><_context.Foo key={_context.bar} /><_context.Foo foo={\\"bar\\"} key={\\"bar\\"} /><_context.Foo foo={\\"bar\\"} />
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Mount\\";
+}
+
+export default function Mount(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform multi include.template 1`] = `
+"
+import _include$1 from \\"./Button.html.twig\\";
+import _include from \\"./Flag.twig\\";
+import { merge } from \\"melody-runtime\\";
+export const _template = {};
+
+_template.render = function (_context) {
+ let tEB_SAME_WINDOW_CLICKOUT, ironItem1p1, bClickoutClientside, sBtnModifiers, flagModifiers, dataVariables;
+ tEB_SAME_WINDOW_CLICKOUT = false;
+ ironItem1p1 = true;
+ bClickoutClientside = false;
+ sBtnModifiers = \\"\\";
+
+ let _child;
+
+ let _child$1, _child$3, _child$4, _child$5, _child$22;
+
+ if (!ironItem1p1) {
+ if (typeof _context.rateCount !== \\"undefined\\") {
+ _child$1 = ie_bestdeal ;
+ }
+ }
+
+ if (_context.bestPriceDeal) {
+ flagModifiers = \\"trv-maincolor-03 font-bright cur-pointer--hover\\";
+
+ if (ironItem1p1) {
+ flagModifiers = \\"font-bright cur-pointer--hover\\";
+ }
+
+ if (_context.isTopDeal) {
+ dataVariables = {
+ \\"data-topdeal-percentage\\": _context.topDealPercentage,
+ \\"data-topdeal-price\\": _context.bestPriceDeal.price,
+ \\"data-topdeal-compare-price\\": _context.topDealComparePrice,
+ \\"data-topdeal-criterion\\": _context.topDealCriterion,
+ \\"data-topdeal-criterion-id\\": _context.topDealCriterionId,
+ \\"data-topdeal-category\\": _context.category,
+ \\"data-topdeal-path-name\\": _context.pathName,
+ \\"data-topdeal-overall-liking\\": _context.overallLiking
+ };
+ flagModifiers = flagModifiers + \\" top_deals js_top_deals \\" + _context.topDealCriterion;
+ dataVariables = merge(dataVariables, {
+ \\"data-topdeal-log\\": \\"1\\"
+ });
+ _child$3 = _include({
+ \\"styleModifier\\": flagModifiers,
+ \\"dataVariables\\": dataVariables,
+ \\"text\\": \\"ie_topdeal\\"
+ });
+ } else {
+ if (_context.iSaving >= 20) {
+ _child$4 = _include({
+ \\"styleModifier\\": flagModifiers,
+ \\"dataVariables\\": dataVariables,
+ \\"text\\": \\"-\\" + _context.saving + \\"%\\"
+ });
+ }
+ }
+ }
+
+ let _child$6;
+
+ let _child$7, _child$10, _child$11, _child$12, _child$14, _child$15, _child$17, _child$20;
+
+ if (false) {
+ let _child$8, _child$9;
+
+ if (_context.bestPriceDeal.groupId == 80 && _context.bestPriceDeal.useLocalizedHotelWebsiteLogo) {
+ _child$8 = \\"book_hotel_website_test\\";
+ } else {
+ _child$9 = _context.bestPriceDeal.sName;
+ }
+
+ _child$7 = {_child$8}{_child$9} ;
+ }
+
+ if (_context.bestPriceDeal) {
+ if (_context.maxPriceDeal) {
+ _child$10 = ;
+ _child$11 = ;
+ _child$12 = {_context.maxPriceDeal.price} ;
+ } else {
+ _child$14 = ;
+ }
+
+ _child$15 = {_context.bestPriceDeal.price} ;
+
+ if (false) {
+ let _child$18, _child$19;
+
+ if (_context.oBestPriceDeal.iGroupId == 80 && _context.oBestPriceDeal.bUseLocalizedHotelWebsiteLogo) {
+ _child$18 = \\"book_hotel_website_test\\";
+ } else {
+ _child$19 = _context.oBestPriceDeal.sName;
+ }
+
+ _child$17 = {_child$18}{_child$19} ;
+ }
+
+ if (true) {
+ sBtnModifiers = \\"btn--deal fl-trailing\\";
+ }
+
+ if (false) {
+ sBtnModifiers = sBtnModifiers + \\" alt\\";
+ }
+ } else if (_context.state == 1) {
+ _child$20 = unavailable_deal
;
+
+ if (true) {
+ sBtnModifiers = \\"btn--icon btn--deal btn--disabled fl-trailing trv-maincolor-04-very-light\\";
+ }
+ } else {
+ if (true) {
+ sBtnModifiers = \\"btn--icon btn--deal btn--disabled fl-trailing trv-maincolor-04-very-light\\";
+ }
+ }
+
+ _child$6 = {_child$7}{_child$10}{_child$11}{_child$12}{_child$14}{_child$15}{_child$17}{_child$20}
;
+ _child$5 = {_child$6}
;
+ _child$22 = _include$1({
+ \\"styleModifier\\": sBtnModifiers,
+ \\"text\\": \\"deals_forward_new\\"
+ });
+ _child = = 20 ? \\"reduced\\" : \\"\\")} itemProp=\\"priceSpecification\\" itemScope itemType=\\"http://schema.org/PriceSpecification\\">{_child$1}{_child$3}{_child$4}{_child$5}{_child$22}
;
+ return ;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"MultiInclude\\";
+}
+
+export default function MultiInclude(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform raw.template 1`] = `
+"
+export const _template = {};
+
+_template.render = function (_context) {
+ let vars;
+
+ let _child, _child$1;
+
+ vars = {
+ \\"foo\\": \\"foo \\",
+ \\"bar\\": bar \\"
+ }} />
+ };
+ _child = ;
+ _child$1 = vars.bar;
+ return {_child}{_child$1}
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Raw\\";
+}
+
+export default function Raw(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform ref.template 1`] = `
+"
+export const _template = {};
+
+_template.render = function (_context) {
+ return udisuabcd test
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Ref\\";
+}
+
+export default function Ref(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform spaceless.template 1`] = `
+"
+export const _template = {};
+
+_template.render = function (_context) {
+ return Receive {_context.formattedIncentive} cash back for testing this hotel.
+ Or just be happy!
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Spaceless\\";
+}
+
+export default function Spaceless(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform styles.template 1`] = `
+"
+import { styles, classes } from \\"melody-runtime\\";
+export const _template = {};
+
+_template.render = function (_context) {
+ return Lorem Ipsum
;
+ return Lorem Ipsum
;
+ return Lorem Ipsum
;
+ return Lorem Ipsum
;
+ return Lorem Ipsum
;
+ return Lorem Ipsum
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Styles\\";
+}
+
+export default function Styles(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should correctly transform svg.template 1`] = `
+"
+export const _template = {};
+
+_template.render = function (_context) {
+ return ;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Svg\\";
+}
+
+export default function Svg(props) {
+ return _template.render(props);
+}
+"
+`;
+
+exports[`Compiler should fail transforming unknown filter.template 1`] = `
+"Unknown filter \\"unknown\\"
+> 1 | {{ test | unknown }}
+ | ^^^^^^^
+
+You've tried to invoke an unknown filter called \\"unknown\\".
+Some of the known filters include:
+
+ - abs
+ - attrs
+ - batch
+ - capitalize
+ - classes
+ - convert_encoding
+ - date
+ - date_modify
+ - default
+ - escape
+ - first
+ - format
+ - join
+ - json_encode
+ - keys
+ - last
+ - length
+ - lower
+ - merge
+ - nl2br
+ - number_format
+ - raw
+ - replace
+ - reverse
+ - round
+ - slice
+ - sort
+ - split
+ - striptags
+ - styles
+ - title
+ - trim
+ - upper
+ - url_encode
+
+Please report this as a bug if the filter you've tried to use is listed here:
+http://twig.sensiolabs.org/doc/filters/index.html"
+`;
diff --git a/packages/melody-plugin-jsx/__tests__/__snapshots__/JSXSpec.js.snap b/packages/melody-plugin-jsx/__tests__/__snapshots__/JSXSpec.js.snap
new file mode 100644
index 0000000..604d5fb
--- /dev/null
+++ b/packages/melody-plugin-jsx/__tests__/__snapshots__/JSXSpec.js.snap
@@ -0,0 +1,251 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`JSX should hoist attributes that will get mutated 1`] = `
+"export const _template = {};
+
+_template.render = function (_context) {
+ let foo;
+ foo = \\"bar\\";
+
+ let _prop = foo,
+ _child;
+
+ foo = \\"qux\\";
+ _child = Hello World
;
+ return {_child}
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+export default function Test(props) {
+ return _template.render(props);
+}"
+`;
+
+exports[`JSX should hoist optimized attributes that will get mutated 1`] = `
+"export const _template = {};
+
+_template.render = function (_context) {
+ let foo;
+ foo = \\"bar\\";
+
+ let _prop = foo,
+ _child;
+
+ foo = \\"qux\\";
+ _child = Hello World
;
+ return {_child}
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+export default function Test(props) {
+ return _template.render(props);
+}"
+`;
+
+exports[`JSX should remove attributes filters 1`] = `
+"export const _template = {};
+
+_template.render = function (_context) {
+ return
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+export default function Test(props) {
+ return _template.render(props);
+}"
+`;
+
+exports[`JSX should spread identifiers 1`] = `
+"export const _template = {};
+
+_template.render = function (_context) {
+ return
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+export default function Test(props) {
+ return _template.render(props);
+}"
+`;
+
+exports[`JSX should support object expression 1`] = `
+"export const _template = {};
+
+_template.render = function (_context) {
+ return
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+export default function Test(props) {
+ return _template.render(props);
+}"
+`;
+
+exports[`JSX should transform attributes to jsx naming conventions 1`] = `
+"export const _template = {};
+
+_template.render = function (_context) {
+ return
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+export default function Test(props) {
+ return _template.render(props);
+}"
+`;
+
+exports[`JSX should transform string literals 1`] = `
+"export const _template = {};
+
+_template.render = function (_context) {
+ return
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+export default function Test(props) {
+ return _template.render(props);
+}"
+`;
+
+exports[`JSX should work with complex conditions 1`] = `
+"export const _template = {};
+
+_template.render = function (_context) {
+ let foo;
+ foo = \\"bar\\";
+
+ let _prop = foo,
+ _child,
+ _child$2,
+ _child$4,
+ _child$6,
+ _child$8;
+
+ foo = \\"qux\\";
+ _child = Hello World
;
+
+ if (_context.bar == \\"foo\\") {
+ foo = \\"nom\\";
+ _child$2 = Hello
;
+ } else if (_context.bar == \\"nom\\") {
+ _child$4 = ;
+
+ if (_context.qux == \\"nom\\") {
+ _child$6 = Qux ;
+ }
+ } else {
+ _child$8 = Look at this {_context.bar}
;
+ }
+
+ return {_child}{_child$2}{_child$4}{_child$6}{_child$8}
;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+export default function Test(props) {
+ return _template.render(props);
+}"
+`;
+
+exports[`JSX should work with loops 1`] = `
+"export const _template = {};
+
+_template.render = function (_context) {
+ let _child = new Array();
+
+ {
+ let _sequence = _context.items,
+ _index = 0,
+ _length = _sequence.length,
+ item = _sequence[0];
+
+ for (; _index < _length; _index++, item = _sequence[_index]) {
+ let className;
+ className = \\"li-\\" + _context.id;
+
+ let _prop = className,
+ _child$1;
+
+ className = \\"span-\\" + _context.id;
+ _child$1 = {item.label} ;
+ _child[_child.length] = {_child$1} ;
+ }
+ }
+ return ;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+export default function Test(props) {
+ return _template.render(props);
+}"
+`;
+
+exports[`JSX should work with loops and multiple roots 1`] = `
+"export const _template = {};
+
+_template.render = function (_context) {
+ let _child = new Array();
+
+ {
+ let _sequence = _context.items,
+ _index = 0,
+ _length = _sequence.length,
+ item = _sequence[0],
+ _index$1 = 1,
+ _revindex = _length,
+ _revindex$1 = _length - 1,
+ _first = true,
+ _last = 1 === _length;
+
+ for (; _index < _length; _index++, item = _sequence[_index]) {
+ _child[_child.length] = Hello World
;
+ _child[_child.length] = Hotel? Trivago! ;
+
+ if (_context.foo) {
+ _child[_child.length] = Foo you! ;
+ }
+
+ _index$1++;
+ _revindex--;
+ _revindex$1--;
+ _first = false;
+ _last = _revindex$1 === 0;
+ }
+ }
+ return ;
+};
+
+if (process.env.NODE_ENV !== \\"production\\") {
+ _template.displayName = \\"Test\\";
+}
+
+export default function Test(props) {
+ return _template.render(props);
+}"
+`;
diff --git a/packages/melody-plugin-jsx/package.json b/packages/melody-plugin-jsx/package.json
new file mode 100644
index 0000000..4de5607
--- /dev/null
+++ b/packages/melody-plugin-jsx/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "melody-plugin-jsx",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; SUPPORT_CJS=true rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "babel-types": "^6.8.1"
+ },
+ "bundledDependencies": [
+ "babel-types"
+ ],
+ "peerDependencies": {
+ "melody-traverse": "^0.10.0",
+ "melody-types": "^0.10.0"
+ },
+ "devDependencies": {
+ "melody-compiler": "^0.11.1-rc.1",
+ "melody-extension-core": "^0.11.1-rc.1",
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-plugin-jsx/src/helpers/index.js b/packages/melody-plugin-jsx/src/helpers/index.js
new file mode 100644
index 0000000..0da179b
--- /dev/null
+++ b/packages/melody-plugin-jsx/src/helpers/index.js
@@ -0,0 +1,160 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+import { is } from 'melody-types';
+import { traverse } from 'melody-traverse';
+import propsMap from './propsMap.js';
+
+export function mapPropName(prop) {
+ const mapped = propsMap[prop];
+ if (mapped) {
+ return mapped;
+ }
+ return prop;
+}
+
+export function prepareNode(path) {
+ const { node } = path;
+ const parentElement = path.findParentPathOfType('Element');
+
+ if (is(node, 'Element')) {
+ const isSelfClosing = node.selfClosing || !node.children.length;
+ if (!isSelfClosing) {
+ path.setData('childNames', []);
+ path.setData('childNamesArrays', []);
+ }
+ }
+
+ if (parentElement && !is(path.parent, 'BlockStatement')) {
+ let childName;
+ const childNames = parentElement.getData('childNames');
+ const childNamesArrays = parentElement.getData('childNamesArrays');
+
+ const parentForStatement = findParentPathOfTypeAndBreakWhen(
+ path,
+ 'ForStatement',
+ path => path && path.is('Element'),
+ );
+
+ const isRootElementOfLoop = !!parentForStatement;
+
+ if (isRootElementOfLoop) {
+ const parentForStatementChildName = parentForStatement.getData(
+ 'childName',
+ );
+ if (!parentForStatementChildName) {
+ childName = parentElement.scope.generateUid('child');
+ parentElement.scope.registerBinding(childName, path, 'let');
+ childNames.push(childName);
+ childNamesArrays.push(isRootElementOfLoop);
+ parentForStatement.setData('childName', childName);
+ } else {
+ childName = parentForStatementChildName;
+ }
+ } else {
+ childName = parentElement.scope.generateUid('child');
+ parentElement.scope.registerBinding(childName, path, 'let');
+ childNames.push(childName);
+ childNamesArrays.push(isRootElementOfLoop);
+ }
+
+ path.setData('childName', childName);
+ path.setData('isRootElementOfLoop', isRootElementOfLoop);
+ }
+}
+
+export function assembleNode(path, expression) {
+ const parentElement = path.findParentPathOfType('Element');
+
+ if (parentElement && !is(path.parent, 'BlockStatement')) {
+ const parentElementContainsStatements = parentElement.getData(
+ 'containsStatements',
+ );
+
+ if (!parentElementContainsStatements) {
+ if (is(expression, 'StringLiteral')) {
+ return t.jSXText(expression.value);
+ }
+ if (is(expression, 'Expression')) {
+ return t.jSXExpressionContainer(expression);
+ }
+ return expression;
+ }
+
+ const childName = path.getData('childName');
+ const isRootElementOfLoop = path.getData('isRootElementOfLoop');
+
+ if (isRootElementOfLoop) {
+ return t.expressionStatement(
+ t.assignmentExpression(
+ '=',
+ t.memberExpression(
+ t.identifier(childName),
+ t.memberExpression(
+ t.identifier(childName),
+ t.identifier('length'),
+ ),
+ true,
+ ),
+ expression,
+ ),
+ );
+ }
+ return t.expressionStatement(
+ t.assignmentExpression('=', t.identifier(childName), expression),
+ );
+ }
+
+ return t.returnStatement(expression);
+}
+
+export function isMutated(path, node) {
+ const binding = path.scope.getBinding(node.name);
+ return binding && binding.mutated;
+}
+
+export function isSaveAttribute(attribute) {
+ let mutated = false;
+ traverse(attribute, {
+ Identifier(path) {
+ mutated = mutated || isMutated(path, path.node);
+ },
+ });
+ return !mutated;
+}
+
+export function getJSXAttributeName(node) {
+ const name = is(node, 'Identifier') ? node.name : node.value;
+ return t.jSXIdentifier(mapPropName(name));
+}
+
+export function getJSXAttributeValue(node) {
+ if (!node) {
+ return null;
+ }
+ return is(node, 'StringLiteral') ? node : t.jSXExpressionContainer(node);
+}
+
+export function findParentPathOfTypeAndBreakWhen(path, type, breakWhen) {
+ let current = path.parentPath;
+ while (current && !current.is(type)) {
+ if (breakWhen(current)) {
+ return null;
+ }
+ current = current.parentPath;
+ }
+ return current && current.type === type ? current : null;
+}
diff --git a/packages/melody-plugin-jsx/src/helpers/propsMap.js b/packages/melody-plugin-jsx/src/helpers/propsMap.js
new file mode 100644
index 0000000..68d3e64
--- /dev/null
+++ b/packages/melody-plugin-jsx/src/helpers/propsMap.js
@@ -0,0 +1,401 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+const LOW = Symbol();
+const props = {
+ acceptCharset: 'accept-charset',
+ accessKey: LOW,
+ allowFullScreen: LOW,
+ allowTransparency: LOW,
+ autoComplete: LOW,
+ autoFocus: LOW,
+ autoPlay: LOW,
+ cellPadding: LOW,
+ cellSpacing: LOW,
+ charSet: LOW,
+ classID: LOW,
+ className: 'class',
+ colSpan: LOW,
+ contentEditable: LOW,
+ contextMenu: LOW,
+ crossOrigin: LOW,
+ dateTime: LOW,
+ encType: LOW,
+ formAction: LOW,
+ formEncType: LOW,
+ formMethod: LOW,
+ formNoValidate: LOW,
+ formTarget: LOW,
+ frameBorder: LOW,
+ hrefLang: LOW,
+ htmlFor: 'for',
+ httpEquiv: 'http-equiv',
+ inputMode: LOW,
+ keyParams: LOW,
+ keyType: LOW,
+ marginHeight: LOW,
+ marginWidth: LOW,
+ maxLength: LOW,
+ mediaGroup: LOW,
+ minLength: LOW,
+ noValidate: LOW,
+ playsInline: LOW,
+ radioGroup: LOW,
+ readOnly: LOW,
+ referrerPolicy: LOW,
+ rowSpan: LOW,
+ spellCheck: LOW,
+ srcDoc: LOW,
+ srcLang: LOW,
+ srcSet: LOW,
+ tabIndex: 'tabindex',
+ // Non-standard Properties
+ autoCapitalize: LOW,
+ autoCorrect: LOW,
+ autoSave: LOW,
+ itemProp: LOW,
+ itemScope: LOW,
+ itemType: LOW,
+ itemID: LOW,
+ itemRef: LOW,
+ // SVG
+ accentHeight: 'accent-height',
+ alignmentBaseline: 'alignment-baseline',
+ allowReorder: 'allow-reorder',
+ arabicForm: 'arabic-form',
+ attributeName: 'attribute-name',
+ attributeType: 'attribute-type',
+ autoReverse: 'auto-reverse',
+ baseFrequency: 'base-frequency',
+ baseProfile: 'base-profile',
+ baselineShift: 'baseline-shift',
+ calcMode: 'calc-mode',
+ capHeight: 'cap-height',
+ clipPath: 'clip-path',
+ clipPathUnits: 'clip-path-units',
+ clipRule: 'clip-rule',
+ colorInterpolation: 'color-interpolation',
+ colorInterpolationFilters: 'color-interpolation-filters',
+ colorProfile: 'color-profile',
+ colorRendering: 'color-rendering',
+ contentScriptType: 'content-script-type',
+ contentStyleType: 'content-style-type',
+ diffuseConstant: 'diffuse-constant',
+ dominantBaseline: 'dominant-baseline',
+ edgeMode: 'edge-mode',
+ enableBackground: 'enable-background',
+ externalResourcesRequired: 'external-resources-required',
+ fillOpacity: 'fill-opacity',
+ fillRule: 'fill-rule',
+ filterRes: 'filter-res',
+ filterUnits: 'filter-units',
+ floodColor: 'flood-color',
+ floodOpacity: 'flood-opacity',
+ fontFamily: 'font-family',
+ fontSize: 'font-size',
+ fontSizeAdjust: 'font-size-adjust',
+ fontStretch: 'font-stretch',
+ fontStyle: 'font-style',
+ fontVariant: 'font-variant',
+ fontWeight: 'font-weight',
+ glyphName: 'glyph-name',
+ glyphOrientationHorizontal: 'glyph-orientation-horizontal',
+ glyphOrientationVertical: 'glyph-orientation-vertical',
+ glyphRef: 'glyph-ref',
+ gradientTransform: 'gradient-transform',
+ gradientUnits: 'gradient-units',
+ horizAdvX: 'horiz-adv-x',
+ horizOriginX: 'horiz-origin-x',
+ imageRendering: 'image-rendering',
+ kernelMatrix: 'kernel-matrix',
+ kernelUnitLength: 'kernel-unit-length',
+ keyPoints: 'key-points',
+ keySplines: 'key-splines',
+ keyTimes: 'key-times',
+ lengthAdjust: 'length-adjust',
+ letterSpacing: 'letter-spacing',
+ lightingColor: 'lighting-color',
+ limitingConeAngle: 'limiting-cone-angle',
+ markerEnd: 'marker-end',
+ markerHeight: 'marker-height',
+ markerMid: 'marker-mid',
+ markerStart: 'marker-start',
+ markerUnits: 'marker-units',
+ markerWidth: 'marker-width',
+ mask: 'mask',
+ maskContentUnits: 'mask-content-units',
+ maskUnits: 'mask-units',
+ numOctaves: 'num-octaves',
+ overlinePosition: 'overline-position',
+ overlineThickness: 'overline-thickness',
+ paintOrder: 'paint-order',
+ pathLength: 'path-length',
+ patternContentUnits: 'pattern-content-units',
+ patternTransform: 'pattern-transform',
+ patternUnits: 'pattern-units',
+ pointerEvents: 'pointer-events',
+ pointsAtX: 'points-at-X',
+ pointsAtY: 'points-at-y',
+ pointsAtZ: 'points-at-z',
+ preserveAlpha: 'preserve-alpha',
+ preserveAspectRatio: 'preserve-aspect-ratio',
+ primitiveUnits: 'primitive-units',
+ refX: 'ref-x',
+ refY: 'ref-y',
+ renderingIntent: 'rendering-intent',
+ repeatCount: 'repeat-count',
+ repeatDur: 'repeat-dur',
+ requiredExtensions: 'required-extensions',
+ requiredFeatures: 'required-features',
+ shapeRendering: 'shape-rendering',
+ specularConstant: 'specular-constant',
+ specularExponent: 'specular-exponent',
+ spreadMethod: 'spread-method',
+ startOffset: 'start-offset',
+ stdDeviation: 'std-deviation',
+ stitchTiles: 'stitch-tiles',
+ stopColor: 'stop-color',
+ stopOpacity: 'stop-opacity',
+ strikethroughPosition: 'strikethrough-position',
+ strikethroughThickness: 'strikethrough-thickness',
+ strokeDasharray: 'stroke-dasharray',
+ strokeDashoffset: 'stroke-dashoffset',
+ strokeLinecap: 'stroke-linecap',
+ strokeLinejoin: 'stroke-linejoin',
+ strokeMiterlimit: 'stroke-miterlimit',
+ strokeOpacity: 'stroke-opacity',
+ strokeWidth: 'stroke-width',
+ surfaceScale: 'surface-scale',
+ systemLanguage: 'system-language',
+ tableValues: 'table-values',
+ targetX: 'target-x',
+ targetY: 'target-y',
+ textAnchor: 'text-anchor',
+ textDecoration: 'text-decoration',
+ textLength: 'text-length',
+ textRendering: 'text-rendering',
+ underlinePosition: 'underline-position',
+ underlineThickness: 'underline-thickness',
+ unicodeBidi: 'unicode-bidi',
+ unicodeRange: 'unicode-range',
+ unitsPerEm: 'units-per-em',
+ vAlphabetic: 'v-alphabetic',
+ vHanging: 'v-hanging',
+ vIdeographic: 'v-ideographic',
+ vMathematical: 'v-mathematical',
+ vectorEffect: 'vector-effect',
+ vertAdvY: 'vert-adv-y',
+ vertOriginX: 'vert-origin-x',
+ vertOriginY: 'vert-origin-y',
+ viewBox: 'view-box',
+ viewTarget: 'view-target',
+ wordSpacing: 'word-spacing',
+ writingMode: 'writing-mode',
+ xChannelSelector: 'x-channel-selector',
+ xHeight: 'x-height',
+ xlinkActuate: 'xlink:actuate',
+ xlinkArcrole: 'xlink:arcrole',
+ xlinkHref: 'xlink:href',
+ xlinkRole: 'xlink:role',
+ xlinkShow: 'xlink:show',
+ xlinkTitle: 'xlink:title',
+ xlinkType: 'xlink:type',
+ xmlnsXlink: 'xmlns:xlink',
+ xmlBase: 'xml:base',
+ xmlLang: 'xml:lang',
+ xmlSpace: 'xml:space',
+ yChannelSelector: 'y-channel-selector',
+ zoomAndPan: 'zoom-and-pan',
+ // Event handlers
+ onAbort: LOW,
+ onAfterprint: LOW,
+ onAnimationEnd: LOW,
+ onAnimationIteration: LOW,
+ onAnimationStart: LOW,
+ onAppInstalled: LOW,
+ onAudioProcess: LOW,
+ onAudioEnd: LOW,
+ onAudioStart: LOW,
+ onBeforePrint: LOW,
+ onBeforeUnload: LOW,
+ onBeginEvent: LOW,
+ onBlocked: LOW,
+ onBlur: LOW,
+ onBoundary: LOW,
+ onCached: LOW,
+ onCanplay: LOW,
+ onCanplayThrough: LOW,
+ onChange: LOW,
+ onChargingChange: LOW,
+ onChargingTimeChange: LOW,
+ onChecking: LOW,
+ onClick: LOW,
+ onClose: LOW,
+ onComplete: LOW,
+ onCompositionEnd: LOW,
+ onCompositionStart: LOW,
+ onCompositionUpdate: LOW,
+ onContextMenu: LOW,
+ onCopy: LOW,
+ onCut: LOW,
+ onDblClick: LOW,
+ onDeviceChange: LOW,
+ onDeviceLight: LOW,
+ onDeviceMotion: LOW,
+ onDeviceOrientation: LOW,
+ onDeviceProximity: LOW,
+ onDischargingTimeChange: LOW,
+ onDOMAttributeNameChanged: LOW,
+ onDOMAttrModified: LOW,
+ onDOMCharacterDataModified: LOW,
+ onDOMContentLoaded: LOW,
+ onDOMElementNameChanged: LOW,
+ onDOMNodeInserted: LOW,
+ onDOMNodeInsertedIntoDocument: LOW,
+ onDOMNodeRemoved: LOW,
+ onDOMNodeRemovedFromDocument: LOW,
+ onDOMSubtreeModified: LOW,
+ onDownloading: LOW,
+ onDrag: LOW,
+ onDragEnd: LOW,
+ onDragEnter: LOW,
+ onDragLeave: LOW,
+ onDragOver: LOW,
+ onDragStart: LOW,
+ onDrop: LOW,
+ onDurationChange: LOW,
+ onEmptied: LOW,
+ onEnd: LOW,
+ onEnded: LOW,
+ onError: LOW,
+ onFocus: LOW,
+ onFocusIn: LOW,
+ onFocusOut: LOW,
+ onFullscreenChange: LOW,
+ onFullscreenError: LOW,
+ onGamepadConnected: LOW,
+ onGamepadDisconnected: LOW,
+ onGotPointerCapture: LOW,
+ onHashChange: LOW,
+ onLostPointerCapture: LOW,
+ onInput: LOW,
+ onInvalid: LOW,
+ onKeydown: LOW,
+ onKeypress: LOW,
+ onKeyup: LOW,
+ onLanguageChange: LOW,
+ onLevelChange: LOW,
+ onLoad: LOW,
+ onLoadEnd: LOW,
+ onLoadStart: LOW,
+ onMark: LOW,
+ onMessage: LOW,
+ onMouseDown: LOW,
+ onMouseEnter: LOW,
+ onMouseLeave: LOW,
+ onMouseMove: LOW,
+ onMouseOut: LOW,
+ onMouseOver: LOW,
+ onMouseUp: LOW,
+ onNoMatch: LOW,
+ onNotificationClick: LOW,
+ onNoUpdate: LOW,
+ onObsolete: LOW,
+ onOffline: LOW,
+ onOnline: LOW,
+ onOpen: LOW,
+ onOrientationChange: LOW,
+ onPageHide: LOW,
+ onPageShow: LOW,
+ onPaste: LOW,
+ onPause: LOW,
+ onPointerCancel: LOW,
+ onPointerDown: LOW,
+ onPointerEnter: LOW,
+ onPointerLeave: LOW,
+ onPointerLockChange: LOW,
+ onPointerLockError: LOW,
+ onPointerMove: LOW,
+ onPointerOut: LOW,
+ onPointerOver: LOW,
+ onPointerUp: LOW,
+ onPlay: LOW,
+ onPlaying: LOW,
+ onPopState: LOW,
+ onProgress: LOW,
+ onPush: LOW,
+ onPushSubscriptionChange: LOW,
+ onRateChange: LOW,
+ onReadyStateChange: LOW,
+ onRepeatEvent: LOW,
+ onReset: LOW,
+ onResize: LOW,
+ onResourceTimingBufferFull: LOW,
+ onResult: LOW,
+ onResume: LOW,
+ onScroll: LOW,
+ onSeeked: LOW,
+ onSeeking: LOW,
+ onSelect: LOW,
+ onSelectStart: LOW,
+ onSelectionChange: LOW,
+ onShow: LOW,
+ onSoundEnd: LOW,
+ onSoundStart: LOW,
+ onSpeechEnd: LOW,
+ onSpeechStart: LOW,
+ onStalled: LOW,
+ onStart: LOW,
+ onStorage: LOW,
+ onSubmit: LOW,
+ onSuccess: LOW,
+ onSuspend: LOW,
+ onSVGAbort: LOW,
+ onSVGError: LOW,
+ onSVGLoad: LOW,
+ onSVGResize: LOW,
+ onSVGScroll: LOW,
+ onSVGUnload: LOW,
+ onSVGZoom: LOW,
+ onTimeout: LOW,
+ onTimeUpdate: LOW,
+ onTouchCancel: LOW,
+ onTouchEnd: LOW,
+ onTouchMove: LOW,
+ onTouchStart: LOW,
+ onTransitionEnd: LOW,
+ onUnload: LOW,
+ onUpdateReady: LOW,
+ onUpgradeNeeded: LOW,
+ onUserProximity: LOW,
+ onVoicesChanged: LOW,
+ onVersionChange: LOW,
+ onVisibilityChange: LOW,
+ onVolumeChange: LOW,
+ onWaiting: LOW,
+ onWheel: LOW,
+};
+
+const propsMap = {};
+for (const prop in props) {
+ const value = props[prop];
+ let key = value;
+ if (value === LOW) {
+ key = prop.toLowerCase();
+ }
+ propsMap[key] = prop;
+}
+
+export default propsMap;
diff --git a/packages/melody-plugin-jsx/src/index.js b/packages/melody-plugin-jsx/src/index.js
new file mode 100644
index 0000000..7c669d0
--- /dev/null
+++ b/packages/melody-plugin-jsx/src/index.js
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+import mountVisitor from './visitors/mount.js';
+import jsxVisitor from './visitors/jsx.js';
+
+export default {
+ visitors: [jsxVisitor, mountVisitor],
+ filterMap: {
+ attrs(path) {
+ const { node } = path;
+ path.replaceWith(node.target);
+ },
+ raw(path) {
+ const jsxElement = t.jSXElement(
+ t.jSXOpeningElement(
+ t.JSXIdentifier('span'),
+ [
+ t.jSXAttribute(
+ t.jSXIdentifier('dangerouslySetInnerHTML'),
+ t.jSXExpressionContainer(
+ t.objectExpression([
+ t.objectProperty(
+ t.identifier('__html'),
+ path.node.target,
+ ),
+ ]),
+ ),
+ ),
+ ],
+ true,
+ ),
+ null,
+ [],
+ true,
+ );
+
+ path.replaceWithJS(jsxElement);
+ },
+ },
+};
diff --git a/packages/melody-plugin-jsx/src/visitors/jsx.js b/packages/melody-plugin-jsx/src/visitors/jsx.js
new file mode 100644
index 0000000..bb61db5
--- /dev/null
+++ b/packages/melody-plugin-jsx/src/visitors/jsx.js
@@ -0,0 +1,254 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+import { is } from 'melody-types';
+import {
+ prepareNode,
+ assembleNode,
+ isMutated,
+ isSaveAttribute,
+ getJSXAttributeName,
+ getJSXAttributeValue,
+} from '../helpers/index.js';
+
+export default {
+ analyse: {
+ Statement: {
+ exit(path) {
+ const { node } = path;
+ if (
+ is(node, 'PrintExpressionStatement') ||
+ is(node, 'PrintTextStatement') ||
+ is(node, 'IncludeStatement') ||
+ is(node, 'BlockStatement') ||
+ is(node, 'EmbedStatement') ||
+ is(node, 'MountStatement')
+ ) {
+ return;
+ }
+ const parentElement = path.findParentPathOfType('Element');
+ if (parentElement && !is(path.parent, 'BlockStatement')) {
+ parentElement.setData('containsStatements', true);
+ }
+ },
+ },
+ Element: {
+ exit(path) {
+ const containsStatements = path.getData('containsStatements');
+ if (containsStatements) {
+ const parentElement = path.findParentPathOfType('Element');
+ if (parentElement && !is(path.parent, 'BlockStatement')) {
+ parentElement.setData('containsStatements', true);
+ }
+ }
+ },
+ },
+ },
+ convert: {
+ PrintStatement: {
+ enter(path) {
+ prepareNode(path);
+ },
+ exit(path) {
+ const { node } = path;
+
+ let jsxElement;
+ const { value } = node;
+ if (is(value, 'ExpressionStatement')) {
+ jsxElement = value.expression;
+ } else {
+ jsxElement = value;
+ }
+
+ path.replaceWithJS(assembleNode(path, jsxElement));
+ },
+ },
+ Element: {
+ enter(path) {
+ prepareNode(path);
+ },
+ exit(path) {
+ const { node } = path;
+ const { attributes } = node;
+ const replacements = [];
+
+ const saveAttributes = [];
+ const unsaveAttributes = [];
+
+ const isSelfClosing = node.selfClosing || !node.children.length;
+ const containsStatements = path.getData('containsStatements');
+
+ function classifyAttribute(node) {
+ if (!containsStatements) {
+ saveAttributes.push(node);
+ return;
+ }
+ const save = is(node, 'Identifier')
+ ? !isMutated(path, node)
+ : isSaveAttribute(node);
+ if (save) {
+ saveAttributes.push(node);
+ } else {
+ unsaveAttributes.push(node);
+ }
+ }
+
+ if (attributes.length) {
+ attributes.forEach(node => {
+ if (is(node, 'StringLiteral')) {
+ saveAttributes.push(node);
+ } else if (is(node, 'ObjectExpression')) {
+ const { properties = [] } = node;
+ properties.forEach(prop => classifyAttribute(prop));
+ } else {
+ classifyAttribute(node);
+ }
+ });
+ }
+
+ const unsaveAttributeNames = unsaveAttributes.map(() => {
+ const name = path.scope.generateUid('prop');
+ path.scope.registerBinding(name, path, 'let');
+ return name;
+ });
+
+ const declarations = unsaveAttributes.map((attribute, idx) => {
+ const name = unsaveAttributeNames[idx];
+ const value = is(attribute, 'Identifier')
+ ? attribute
+ : attribute.value;
+ return t.variableDeclarator(t.identifier(name), value);
+ });
+
+ const childNames = path.getData('childNames');
+ const childNamesArrays = path.getData('childNamesArrays');
+ if (containsStatements) {
+ if (childNames.length) {
+ childNames.forEach((childName, idx) => {
+ const isArray = childNamesArrays[idx];
+ declarations.push(
+ t.variableDeclarator(
+ t.identifier(childName),
+ isArray
+ ? t.newExpression(
+ t.identifier('Array'),
+ [],
+ )
+ : undefined,
+ ),
+ );
+ });
+ }
+ if (declarations.length) {
+ replacements.push(
+ t.variableDeclaration('let', declarations),
+ );
+ }
+
+ // push AST children, here the actual children will be generated
+ replacements.push(...node.children);
+ }
+
+ const properties = [];
+
+ saveAttributes.forEach(node => {
+ if (is(node, 'StringLiteral')) {
+ properties.push(
+ t.jSXAttribute(getJSXAttributeName(node), null),
+ );
+ } else if (is(node, 'Attribute')) {
+ properties.push(
+ t.jSXAttribute(
+ getJSXAttributeName(node.name),
+ getJSXAttributeValue(node.value),
+ ),
+ );
+ } else if (is(node, 'ObjectProperty')) {
+ properties.push(
+ t.jSXAttribute(
+ getJSXAttributeName(node.key),
+ getJSXAttributeValue(node.value),
+ ),
+ );
+ } else {
+ properties.push(t.jSXSpreadAttribute(node));
+ }
+ });
+
+ unsaveAttributes.forEach((node, idx) => {
+ const identifierName = unsaveAttributeNames[idx];
+ const identifier = t.identifier(identifierName);
+ if (is(node, 'Attribute')) {
+ properties.push(
+ t.jSXAttribute(
+ getJSXAttributeName(node.name),
+ t.JSXExpressionContainer(identifier),
+ ),
+ );
+ } else if (is(node, 'ObjectProperty')) {
+ properties.push(
+ t.jSXAttribute(
+ getJSXAttributeName(node.key),
+ t.JSXExpressionContainer(identifier),
+ ),
+ );
+ } else {
+ properties.push(t.jSXSpreadAttribute(identifier));
+ }
+ });
+
+ let jsxElement;
+ if (isSelfClosing) {
+ jsxElement = t.jSXElement(
+ t.jSXOpeningElement(
+ t.JSXIdentifier(node.name),
+ properties,
+ true,
+ ),
+ null,
+ [],
+ true,
+ );
+ } else {
+ jsxElement = t.jSXElement(
+ t.jSXOpeningElement(
+ t.JSXIdentifier(node.name),
+ properties,
+ ),
+ t.jSXClosingElement(t.JSXIdentifier(node.name)),
+ containsStatements
+ ? childNames.map(n =>
+ t.jSXExpressionContainer(t.identifier(n)),
+ )
+ : node.children,
+ );
+ }
+
+ replacements.push(assembleNode(path, jsxElement));
+
+ path.replaceWithMultipleJS(...replacements);
+ },
+ },
+ Fragment: {
+ enter(path) {
+ prepareNode(path);
+ },
+ exit(path) {
+ path.replaceWithJS(assembleNode(path, path.node.value));
+ },
+ },
+ },
+};
diff --git a/packages/melody-plugin-jsx/src/visitors/mount.js b/packages/melody-plugin-jsx/src/visitors/mount.js
new file mode 100644
index 0000000..587568b
--- /dev/null
+++ b/packages/melody-plugin-jsx/src/visitors/mount.js
@@ -0,0 +1,113 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as t from 'babel-types';
+import { is } from 'melody-types';
+import { prepareNode, assembleNode } from '../helpers/index.js';
+
+function toJSXMemberExpression(memberExpression) {
+ const { object, property } = memberExpression;
+ return t.jSXMemberExpression(
+ is(object, 'MemberExpression')
+ ? toJSXMemberExpression(object)
+ : t.jSXIdentifier(object.name),
+ t.jSXIdentifier(property.name),
+ );
+}
+
+export default {
+ convert: {
+ MountStatement: {
+ enter(path) {
+ prepareNode(path);
+ },
+ exit(path) {
+ let componentName;
+ if (path.node.source) {
+ const source = path.node.source.value;
+ let localName;
+ if (path.node.name) {
+ localName = this.addImportFrom(
+ source,
+ path.node.name.name,
+ );
+ } else {
+ localName = this.addDefaultImportFrom(
+ source,
+ // JSX expects component names to be capitalized, otherwise
+ // it assumes it's an html element
+ this.generateComponentUid(source),
+ );
+ }
+ path.scope.registerBinding(localName, path, 'var');
+ this.markIdentifier(localName);
+ componentName = t.identifier(localName);
+ } else {
+ componentName = path.node.name;
+ }
+
+ const replacements = [];
+
+ const attributes = path.node.argument;
+ let properties = [];
+ if (attributes) {
+ if (is(attributes, 'ObjectExpression')) {
+ properties = attributes.properties.map(
+ ({ key, value }) => {
+ return t.jSXAttribute(
+ t.jSXIdentifier(
+ is(key, 'Identifier')
+ ? key.name
+ : key.value,
+ ),
+ t.jSXExpressionContainer(value),
+ );
+ },
+ );
+ } else {
+ properties = [t.jSXSpreadAttribute(attributes)];
+ }
+ }
+
+ if (path.node.key) {
+ properties.push(
+ t.jSXAttribute(
+ t.jSXIdentifier('key'),
+ t.jSXExpressionContainer(path.node.key),
+ ),
+ );
+ }
+
+ let identifier = componentName;
+ if (is(identifier, 'MemberExpression')) {
+ identifier = toJSXMemberExpression(identifier);
+ } else {
+ identifier = t.jSXIdentifier(identifier.name);
+ }
+
+ const jsxElement = t.jSXElement(
+ t.jSXOpeningElement(identifier, properties, true),
+ null,
+ [],
+ true,
+ );
+
+ replacements.push(assembleNode(path, jsxElement));
+
+ path.replaceWithMultipleJS(...replacements);
+ },
+ },
+ },
+};
diff --git a/packages/melody-redux/__tests__/ConnectSpec.js b/packages/melody-redux/__tests__/ConnectSpec.js
new file mode 100644
index 0000000..ed78aa4
--- /dev/null
+++ b/packages/melody-redux/__tests__/ConnectSpec.js
@@ -0,0 +1,580 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { assert } from 'chai';
+
+import { createComponent, RECEIVE_PROPS, render } from 'melody-component';
+import {
+ component,
+ elementOpen,
+ elementClose,
+ elementVoid,
+ text,
+ flush,
+} from 'melody-idom';
+import { provide, connect } from '../src/';
+import { createStore } from 'redux';
+
+function reducer(state = { value: 'foo' }, action) {
+ switch (action.type) {
+ case 'SET':
+ return { value: action.payload };
+ default:
+ return state;
+ }
+}
+
+describe('Connect', function() {
+ let store;
+ beforeEach(() => (store = createStore(reducer)));
+ function renderWithProvide(Comp, props) {
+ const template = {
+ render(_context) {
+ elementOpen('div', 'outer');
+ component(Comp, 'test', props);
+ elementClose('div');
+ },
+ };
+ const App = createComponent(template);
+ const ProvidedApp = provide(store, App);
+ const root = document.createElement('div');
+ render(root, ProvidedApp, {});
+ return root;
+ }
+
+ describe('without internal state', () => {
+ let Component;
+ const template = {
+ render(_context) {
+ elementOpen('div', 'inner');
+ text(_context.value);
+ elementClose('div');
+ },
+ };
+
+ beforeEach(() => {
+ Component = createComponent(template);
+ });
+
+ it('should render component with state from redux store', done => {
+ const enhance = connect(state => ({ value: state.value + '!' }));
+ const ConnectedComponent = enhance(Component);
+
+ const dom = renderWithProvide(ConnectedComponent);
+ assert.equal(dom.outerHTML, '');
+ store.dispatch({ type: 'SET', payload: 'bar' });
+
+ finishRendering();
+ assert.equal(dom.outerHTML, '');
+ done();
+ });
+
+ it('should support multiple connect higher order components', done => {
+ const enhance = connect(state => ({ value: state.value + '!' }));
+ const enhance2 = connect(state => ({ value: state.value + '?' }));
+ const ConnectedComponent = enhance2(enhance(Component));
+
+ const dom = renderWithProvide(ConnectedComponent);
+
+ assert.equal(dom.outerHTML, '');
+ store.dispatch({ type: 'SET', payload: 'bar' });
+
+ finishRendering();
+ assert.equal(dom.outerHTML, '');
+ done();
+ });
+
+ it('should support multiple connect higher order components and receive their state as props', done => {
+ const enhance = connect((state, props) => ({
+ value: props.value + '!',
+ }));
+ const enhance2 = connect(state => ({ value: state.value + '?' }));
+ const ConnectedComponent = enhance2(enhance(Component));
+
+ const dom = renderWithProvide(ConnectedComponent);
+
+ assert.equal(dom.outerHTML, '');
+ store.dispatch({ type: 'SET', payload: 'bar' });
+
+ finishRendering();
+ assert.equal(dom.outerHTML, '');
+ done();
+ });
+
+ // it('should not drop wrapped component when shouldComponentUpdate returns false', done => {
+ // const enhance = connect((state) => ({ value: state.value + '!' }));
+ // const ConnectedComponent = enhance(Component)({
+ // shouldComponentUpdate() {
+ // return false;
+ // }
+ // });
+
+ // const template = {
+ // render(_context) {
+ // elementOpen('div', 'outer');
+ // component(ConnectedComponent, 'test', {});
+ // elementClose('div');
+ // }
+ // };
+ // const App = createComponent(template);
+ // const ProvidedApp = provide(store, App);
+ // const dom = document.createElement('div');
+ // render(dom, ProvidedApp, { foo: 1 });
+
+ // assert.equal(dom.outerHTML, '');
+ // render(dom, ProvidedApp, { foo: 2 });
+
+ // assert.equal(dom.outerHTML, '');
+ // done();
+ // });
+
+ it('should pass props from connected component to child component', () => {
+ const enhance = connect();
+ const ConnectedComponent = enhance(Component);
+
+ const dom = renderWithProvide(ConnectedComponent, { value: 'bar' });
+ assert.equal(dom.outerHTML, '');
+ });
+
+ it('should have higher priority for stateToProps than component props', () => {
+ const enhance = connect(state => ({ value: `${state.value}!` }));
+ const ConnectedComponent = enhance(Component);
+
+ const dom = renderWithProvide(ConnectedComponent, { value: 'bar' });
+ assert.equal(dom.outerHTML, '');
+ });
+
+ it('should map dispatchable action to props', done => {
+ const enhance = connect(
+ state => {
+ return { value: state.value };
+ },
+ {
+ setValue: val => {
+ return { type: 'SET', payload: val };
+ },
+ },
+ );
+ let dom;
+ const Component = createComponent(
+ template,
+ null,
+ ({ componentDidMount }) => ({
+ componentDidMount() {
+ componentDidMount.call(this);
+ //simulate async event
+ setTimeout(() => {
+ this.getState().setValue('bar');
+ finishRendering();
+ assert.equal(
+ dom.outerHTML,
+ '',
+ );
+ done();
+ }, 1);
+ },
+ }),
+ );
+ const ConnectedComponent = enhance(Component);
+ dom = renderWithProvide(ConnectedComponent);
+ });
+
+ it('should be able to take a function to map actions to props', done => {
+ const enhance = connect(
+ state => {
+ return { value: state.value };
+ },
+ (dispatch, props) => {
+ return {
+ setValue: val => {
+ dispatch({
+ type: 'SET',
+ payload: `${val}_${props.suffix}`,
+ });
+ },
+ };
+ },
+ );
+ let dom;
+ const Component = createComponent(
+ template,
+ null,
+ ({ componentDidMount }) => ({
+ componentDidMount() {
+ componentDidMount.call(this);
+ //simulate async event
+ setTimeout(() => {
+ this.getState().setValue('bar');
+ finishRendering();
+ assert.equal(
+ dom.outerHTML,
+ '',
+ );
+ done();
+ }, 1);
+ },
+ }),
+ );
+ const ConnectedComponent = enhance(Component);
+ dom = renderWithProvide(ConnectedComponent, { suffix: 'melody' });
+ });
+
+ it('should be able to call action from componentDidMount', done => {
+ const enhance = connect(
+ state => {
+ return { value: state.value };
+ },
+ {
+ setValue: val => {
+ return { type: 'SET', payload: val };
+ },
+ },
+ );
+ const Component = createComponent(
+ template,
+ undefined,
+ ({ componentDidMount }) => ({
+ componentDidMount() {
+ componentDidMount.call(this);
+ assert.equal(this.el.outerHTML, 'foo
');
+ this.getState().setValue('bar');
+ finishRendering();
+ assert.equal(this.el.outerHTML, 'bar
');
+ done();
+ },
+ }),
+ );
+ const ConnectedComponent = enhance(Component);
+ renderWithProvide(ConnectedComponent);
+ });
+ });
+
+ describe('with internal state', () => {
+ let Component, instance;
+ const template = {
+ render(_context) {
+ elementOpen('div', 'inner');
+ text(`${_context.value} `);
+ text(_context.internalValue);
+ elementClose('div');
+ },
+ };
+
+ function internalReducer(state = { internalValue: 'foo' }, action) {
+ switch (action.type) {
+ case RECEIVE_PROPS:
+ return {
+ ...state,
+ ...action.payload,
+ };
+ case 'SET_INTERNAL':
+ return {
+ ...state,
+ internalValue: action.payload,
+ };
+ default:
+ return state;
+ }
+ }
+
+ const instanceMixin = ({ componentWillMount }) => ({
+ componentWillMount() {
+ componentWillMount.call(this);
+ instance = this;
+ },
+ });
+
+ beforeEach(() => {
+ Component = createComponent(
+ template,
+ internalReducer,
+ instanceMixin,
+ );
+ });
+
+ it('should be able for the wrapped component to change its internal state', done => {
+ const enhance = connect(state => ({ value: state.value + '!' }));
+ const ConnectedComponent = enhance(Component);
+
+ const dom = renderWithProvide(ConnectedComponent);
+ assert.equal(dom.outerHTML, '');
+ instance.dispatch({ type: 'SET_INTERNAL', payload: 'bar' });
+ finishRendering();
+ assert.equal(dom.outerHTML, '');
+ done();
+ });
+
+ it('should render after both internal and external changes', done => {
+ const enhance = connect(state => ({ value: state.value + '!' }));
+ const ConnectedComponent = enhance(Component);
+
+ const dom = renderWithProvide(ConnectedComponent);
+ instance.dispatch({ type: 'SET_INTERNAL', payload: 'bar' });
+ store.dispatch({ type: 'SET', payload: 'qux' });
+ finishRendering();
+
+ assert.equal(dom.outerHTML, '');
+ done();
+ });
+
+ it('should conditionally render connected component', done => {
+ const enhance = connect(state => ({ value: state.value + '!' }));
+ const ChildComponent = enhance(
+ createComponent({
+ render(_context) {
+ elementOpen('div');
+ text(_context.value);
+ elementClose('div');
+ },
+ }),
+ );
+
+ const template = {
+ render(_context) {
+ elementOpen('div');
+ if (_context.showChild) {
+ component(ChildComponent, 'child');
+ }
+ elementClose('div');
+ },
+ };
+
+ const reducer = (state = { showChild: false }, action) => {
+ switch (action.type) {
+ case RECEIVE_PROPS:
+ return {
+ ...state,
+ ...action.payload,
+ };
+ case 'SHOW':
+ return {
+ ...state,
+ showChild: true,
+ };
+ default:
+ return state;
+ }
+ };
+
+ const Component = createComponent(template, reducer, instanceMixin);
+ const dom = renderWithProvide(Component);
+ assert.equal(dom.outerHTML, '');
+ instance.dispatch({ type: 'SHOW' });
+ finishRendering();
+ assert.equal(
+ dom.outerHTML,
+ '',
+ );
+ done();
+ });
+ });
+
+ describe('factory functions', () => {
+ it('should allow providing a factory function to mapStateToProps', () => {
+ let factoryCallCount = 0;
+ let updatedCount = 0;
+ let memoizedReturnCount = 0;
+
+ const createMapStateToProps = () => {
+ let propPrev;
+ let valuePrev;
+ let resultPrev;
+ factoryCallCount++;
+ return (state, props) => {
+ if (props.name === propPrev && valuePrev === state.value) {
+ memoizedReturnCount++;
+ return resultPrev;
+ }
+ propPrev = props.name;
+ valuePrev = state.value;
+ return (resultPrev = {
+ someObject: { prop: props.name, value: state.value },
+ });
+ };
+ };
+
+ const containerTemplate = {
+ render(_context) {
+ elementOpen('span');
+ text(_context.someObject.prop);
+ text(_context.someObject.value);
+ elementClose('span');
+ },
+ };
+ const ContainerDumb = createComponent(
+ containerTemplate,
+ null,
+ ({ componentDidUpdate }) => {
+ return {
+ componentDidUpdate(...args) {
+ updatedCount++;
+ componentDidUpdate.call(this, ...args);
+ },
+ };
+ },
+ );
+ const enhance = connect(createMapStateToProps);
+ const Container = enhance(ContainerDumb);
+
+ const appTemplate = {
+ render(_context) {
+ elementOpen('div');
+ component(Container, 'a', { name: 'a' });
+ component(Container, 'b', { name: 'b' });
+ elementClose('div');
+ },
+ };
+ const App = createComponent(appTemplate);
+ renderWithProvide(App);
+ finishRendering();
+ store.dispatch({ type: 'SET', payload: 'foo' });
+ finishRendering();
+ assert.equal(updatedCount, 0);
+ assert.equal(memoizedReturnCount, 2);
+ assert.equal(factoryCallCount, 2);
+ });
+
+ it('should allow providing a factory function to mapDispatchToProps', () => {
+ let updatedCount = 0;
+ let memoizedReturnCount = 0;
+ let factoryCallCount = 0;
+
+ const mapStateToProps = (state, props) => ({
+ value: state.value,
+ });
+
+ const createMapDispatchToProps = () => {
+ let propPrev;
+ let resultPrev;
+ factoryCallCount++;
+ return (dispatch, props) => {
+ if (props.name === propPrev) {
+ memoizedReturnCount++;
+ return resultPrev;
+ }
+ propPrev = props.name;
+ return (resultPrev = {
+ someObject: { dispatchFn: dispatch },
+ });
+ };
+ };
+ function mergeParentDispatch(
+ stateProps,
+ dispatchProps,
+ parentProps,
+ ) {
+ return {
+ ...stateProps,
+ ...dispatchProps,
+ name: parentProps.name,
+ };
+ }
+
+ const containerTemplate = {
+ render(_context) {
+ elementOpen('span');
+ text(_context.value);
+ text(_context.name);
+ elementClose('span');
+ },
+ };
+ const ContainerDumb = createComponent(
+ containerTemplate,
+ null,
+ ({ componentDidUpdate }) => ({
+ componentDidUpdate(...args) {
+ updatedCount++;
+ componentDidUpdate.call(this, ...args);
+ },
+ }),
+ );
+ const enhance = connect(
+ mapStateToProps,
+ createMapDispatchToProps,
+ mergeParentDispatch,
+ );
+ const Container = enhance(ContainerDumb);
+
+ const appTemplate = {
+ render(_context) {
+ elementOpen('div');
+ component(Container, 'a', { name: 'a', odd: _context.odd });
+ component(Container, 'b', { name: 'b', odd: _context.odd });
+ elementClose('div');
+ },
+ };
+ function appReducer(state = { odd: false }, action) {
+ switch (action.type) {
+ case 'TOGGLE':
+ return { odd: !state.odd };
+ default:
+ return state;
+ }
+ }
+ const App = createComponent(
+ appTemplate,
+ appReducer,
+ ({ componentDidMount }) => ({
+ componentDidMount() {
+ componentDidMount.call(this);
+ this.dispatch({ type: 'TOGGLE' });
+ },
+ }),
+ );
+ renderWithProvide(App);
+ finishRendering();
+ store.dispatch({ type: 'SET', payload: 'bar' });
+ finishRendering();
+ assert.equal(updatedCount, 2);
+ assert.equal(memoizedReturnCount, 2);
+ assert.equal(factoryCallCount, 2);
+ });
+ });
+
+ it('should render nested connected components', done => {
+ const enhance = connect(state => ({ value: state.value }));
+ const childTemplate = {
+ render(_context) {
+ elementOpen('div');
+ text(_context.value);
+ elementClose('div');
+ },
+ };
+ const ChildComponent = enhance(createComponent(childTemplate));
+
+ const template = {
+ render(_context) {
+ elementOpen('div');
+ component(ChildComponent, 'child');
+ elementClose('div');
+ },
+ };
+ const Component = enhance(createComponent(template));
+
+ const dom = renderWithProvide(Component);
+
+ assert.equal(dom.outerHTML, '');
+ done();
+ });
+});
+
+function finishRendering() {
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+}
diff --git a/packages/melody-redux/__tests__/ShouldComponentUpdateConnectSpec.js b/packages/melody-redux/__tests__/ShouldComponentUpdateConnectSpec.js
new file mode 100644
index 0000000..e0a7a69
--- /dev/null
+++ b/packages/melody-redux/__tests__/ShouldComponentUpdateConnectSpec.js
@@ -0,0 +1,149 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { assert } from 'chai';
+import { createStore } from 'redux';
+import { createComponent, render } from 'melody-component';
+import { component, elementOpen, elementClose, text, flush } from 'melody-idom';
+import { connect, provide } from '../src';
+
+// When trying to reproduce another bug, I found this where
+// component['MELODY/DATA'] is undefined
+it('should work when connected component is not rendered through typical render pipeline', done => {
+ const store = createStore((state = ['child0'], action) => {
+ if (action.type === 'REFRESH') {
+ return state.concat();
+ }
+ return state;
+ });
+
+ const childTemplate = {
+ render() {
+ elementOpen('span');
+ text('child');
+ elementClose('span');
+ },
+ };
+
+ const ChildComponent = createComponent(childTemplate, null, () => ({
+ componentShouldUpdate(nextProps, nextState) {
+ return nextState !== this.state;
+ },
+ }));
+
+ const ConnectedChild = connect(() => ({}))(ChildComponent);
+
+ const parentTemplate = {
+ render(_context) {
+ elementOpen('li');
+ _context.children.forEach(child => {
+ component(ConnectedChild, child);
+ });
+ elementClose('li');
+ },
+ };
+
+ const ParentComponent = createComponent(parentTemplate, undefined);
+
+ const App = createComponent({
+ render(_context) {
+ elementOpen('ol');
+ component(ParentComponent, 'parent', {
+ children: _context.children,
+ });
+ elementClose('ol');
+ },
+ });
+
+ const ConnectedApp = connect(state => ({
+ children: state,
+ }))(App);
+
+ const ProvidedApp = provide(store, ConnectedApp);
+
+ const root = document.createElement('ol');
+
+ render(root, ProvidedApp);
+ assert.equal(root.outerHTML, 'child ');
+ store.dispatch({ type: 'REFRESH' });
+ // setTimeout(() => {
+ assert.equal(root.outerHTML, 'child ');
+ done();
+ // }, 500);
+});
+
+it('should not delete child component if child did not need rendering', done => {
+ const store = createStore((state = ['child0'], action) => {
+ if (action.type === 'REFRESH') {
+ return state.concat();
+ }
+ return state;
+ });
+
+ const childTemplate = {
+ render(_context) {
+ elementOpen('span');
+ text(_context.length);
+ elementClose('span');
+ },
+ };
+
+ const ChildComponent = createComponent(childTemplate, null, () => ({
+ shouldComponentUpdate(nextProps, nextState) {
+ return this.state.length !== nextState.length;
+ },
+ }));
+
+ const ConnectedChild = connect(state => ({
+ children: state,
+ length: state.length,
+ }))(ChildComponent);
+
+ const parentTemplate = {
+ render(_context) {
+ elementOpen('li');
+ _context.children.forEach(child => {
+ component(ConnectedChild, child, {});
+ });
+ elementClose('li');
+ },
+ };
+
+ const ParentComponent = createComponent(parentTemplate);
+
+ const ProvidedApp = provide(store, ParentComponent);
+
+ const root = document.createElement('li');
+
+ render(root, ProvidedApp, { children: store.getState() });
+ assert.equal(root.outerHTML, '1 ');
+
+ // trigger dispatch on ConnectedChild
+ // ParentComponent does not receive a dispatch
+
+ // ConnectedChild is rendered using renderComponent
+ // ChildComponent is rendered through renderTemplate
+ // ChildComponent does not need rendering (shouldComponentUpdate)
+ // ConnectedChild deletes ChildComponent (delete unvisited nodes)
+ store.dispatch({ type: 'REFRESH' });
+ flush({
+ didTimeout: false,
+ timeRemaining() {
+ return 10;
+ },
+ });
+ assert.equal(root.outerHTML, '1 ');
+ done();
+});
diff --git a/packages/melody-redux/package.json b/packages/melody-redux/package.json
new file mode 100644
index 0000000..1d03132
--- /dev/null
+++ b/packages/melody-redux/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "melody-redux",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "melody-component": "^0.11.1-rc.1"
+ },
+ "peerDependencies": {
+ "melody-idom": "^0.10.0"
+ },
+ "devDependencies": {
+ "redux": "^3.6.0",
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-redux/src/WrapperComponent.js b/packages/melody-redux/src/WrapperComponent.js
new file mode 100644
index 0000000..b945cce
--- /dev/null
+++ b/packages/melody-redux/src/WrapperComponent.js
@@ -0,0 +1,37 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { link } from 'melody-idom';
+
+const createWrapperComponent = Component =>
+ class WrapperComponent {
+ constructor() {
+ this.refs = Object.create(null);
+ this.props = null;
+ this.childInstance = new Component();
+ link(this, this.childInstance);
+ }
+
+ set el(el) {
+ this.childInstance.el = el;
+ return el;
+ }
+
+ get el() {
+ return this.childInstance.el;
+ }
+ };
+
+export { createWrapperComponent };
diff --git a/packages/melody-redux/src/index.js b/packages/melody-redux/src/index.js
new file mode 100644
index 0000000..c02d40d
--- /dev/null
+++ b/packages/melody-redux/src/index.js
@@ -0,0 +1,251 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import type { ReduxStore, Action } from 'melody-component';
+import type { Component } from 'melody-component/component';
+import { getParent } from 'melody-idom';
+import { createWrapperComponent } from './WrapperComponent';
+
+export type StateToPropsMapper = (state: Object, ownProps: Object) => Object;
+export type DispatchToPropsMapper = (
+ dispatch: (action: Action) => void,
+) => Object;
+export type PropsMerger = (
+ stateProps: Object,
+ dispatchProps: Object,
+ ownProps: Object,
+) => Object;
+
+const findNextStore = comp => {
+ for (let node = comp; node; node = getParent(node)) {
+ if (node.store) {
+ return node.store;
+ }
+ }
+ return null;
+};
+
+const defaultEmptyStateToPropsMapping = {};
+const defaultMapStateToProps: StateToPropsMapper = () =>
+ defaultEmptyStateToPropsMapping;
+const defaultMapDispatchToProps: DispatchToPropsMapper = dispatch => ({
+ dispatch,
+});
+const defaultMergeProps: PropsMerger = (
+ stateProps,
+ dispatchProps,
+ parentProps,
+) => Object.assign({}, parentProps, stateProps, dispatchProps);
+
+export function connect(
+ mapStateToProps: ?StateToPropsMapper,
+ mapDispatchToProps: ?(DispatchToPropsMapper | Object),
+ mergeProps: ?PropsMerger,
+) {
+ const shouldSubscribeToStore = !!mapStateToProps;
+ const doStatePropsDependOnOwnProps =
+ shouldSubscribeToStore && mapStateToProps.length !== 1;
+ let finalMapDispatchToProps =
+ mapDispatchToProps || defaultMapDispatchToProps;
+ if (mapDispatchToProps && typeof mapDispatchToProps !== 'function') {
+ const dispatchMap = mapDispatchToProps;
+ finalMapDispatchToProps = dispatch =>
+ Object.keys(dispatchMap).reduce((acc, key) => {
+ acc[key] = (...args) => dispatch(dispatchMap[key](...args));
+ return acc;
+ }, {});
+ }
+ const doDispatchPropsDependOnOwnProps =
+ finalMapDispatchToProps.length !== 1;
+ const finalMapStateToProps = mapStateToProps || defaultMapStateToProps;
+ const finalMergeProps = mergeProps || defaultMergeProps;
+
+ return Component =>
+ class ConnectWrapperComponent extends createWrapperComponent(
+ Component,
+ ) {
+ constructor() {
+ super();
+ this.store = null;
+ this.storeConnection = null;
+
+ this.renderProps = null;
+ this.ownProps = null;
+ this.stateProps = null;
+ this.dispatchProps = null;
+
+ this.mapStateToPropsFn = finalMapStateToProps;
+ this.mapDispatchToPropsFn = finalMapDispatchToProps;
+
+ if (process.env.NODE_ENV !== 'production') {
+ this.displayName = `connect`;
+ }
+ }
+
+ apply(props: Object) {
+ if (shouldSubscribeToStore && !this.storeConnection) {
+ this.subscribeToStore();
+ }
+ if (shallowEquals(this.ownProps, props)) {
+ return;
+ }
+ const store = this.store || findNextStore(this);
+ if (this.ownProps !== props || this.renderProps === null) {
+ this.ownProps = props;
+ if (doStatePropsDependOnOwnProps || !this.stateProps) {
+ this.stateProps = this.mapStateToProps(
+ store.getState(),
+ props,
+ );
+ }
+
+ if (
+ doDispatchPropsDependOnOwnProps ||
+ !this.dispatchProps
+ ) {
+ this.dispatchProps = this.mapDispatchToProps(
+ store.dispatch,
+ props,
+ );
+ }
+
+ this.renderProps = finalMergeProps(
+ this.stateProps,
+ this.dispatchProps,
+ props,
+ );
+ }
+ this.childInstance.apply(this.renderProps);
+ }
+
+ subscribeToStore() {
+ if (shouldSubscribeToStore && !this.storeConnection) {
+ if (!this.store) {
+ this.store = findNextStore(this);
+ }
+ this.storeConnection = this.store.subscribe(() => {
+ const props = this.ownProps;
+ const newStateProps = this.mapStateToProps(
+ this.store.getState(),
+ props,
+ );
+ if (newStateProps !== this.stateProps) {
+ const newRenderProps = finalMergeProps(
+ newStateProps,
+ this.dispatchProps,
+ props,
+ );
+ this.stateProps = newStateProps;
+ const didRenderPropsChange = !shallowEquals(
+ this.renderProps,
+ newRenderProps,
+ );
+ this.renderProps = newRenderProps;
+ if (didRenderPropsChange) {
+ this.childInstance.apply(this.renderProps);
+ }
+ }
+ });
+ }
+ }
+
+ mapStateToProps(state, props) {
+ let result = this.mapStateToPropsFn.call(null, state, props);
+ if (typeof result === 'function') {
+ this.mapStateToPropsFn = result;
+ result = this.mapStateToPropsFn.call(null, state, props);
+ }
+ return result;
+ }
+
+ mapDispatchToProps(dispatch, props) {
+ let result = this.mapDispatchToPropsFn.call(
+ null,
+ dispatch,
+ props,
+ );
+ if (typeof result === 'function') {
+ this.mapDispatchToPropsFn = result;
+ result = this.mapDispatchToPropsFn.call(
+ null,
+ dispatch,
+ props,
+ );
+ }
+ return result;
+ }
+
+ componentWillUnmount() {
+ if (shouldSubscribeToStore) {
+ this.storeConnection();
+ this.storeConnection = null;
+ this.store = null;
+ }
+ this.mapStateToPropsFn = null;
+ this.mapDispatchToPropsFn = null;
+ }
+ };
+}
+
+export function provide(store: ReduxStore, Component: Component): Component {
+ return class StoreProviderComponent extends createWrapperComponent(
+ Component,
+ ) {
+ constructor() {
+ super();
+ this.store = store;
+
+ if (process.env.NODE_ENV !== 'production') {
+ this.displayName = `provide`;
+ }
+ }
+
+ apply(props) {
+ if (!shallowEquals(props, this.props)) {
+ this.childInstance.apply(props);
+ }
+ }
+
+ componentWillUnmount() {}
+ };
+}
+
+const hasOwn = Object.prototype.hasOwnProperty;
+
+// based on react-redux
+function shallowEquals(a, b) {
+ if (a === b) {
+ return true;
+ }
+
+ if (!a || !b) {
+ return false;
+ }
+
+ const keyOfA = Object.keys(a),
+ keysOfB = Object.keys(b);
+
+ if (keyOfA.length !== keysOfB.length) {
+ return false;
+ }
+
+ for (let i = 0; i < keyOfA.length; i++) {
+ if (!hasOwn.call(b, keyOfA[i]) || a[keyOfA[i]] !== b[keyOfA[i]]) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/packages/melody-runtime/__tests__/ContextSpec.js b/packages/melody-runtime/__tests__/ContextSpec.js
new file mode 100644
index 0000000..2ad2fa7
--- /dev/null
+++ b/packages/melody-runtime/__tests__/ContextSpec.js
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { expect } from 'chai';
+
+import { createSubContext } from '../src';
+
+describe('createSubContext', function() {
+ let context = null;
+ beforeEach(function() {
+ context = {
+ a: 2,
+ b: 42,
+ };
+ });
+
+ afterEach(function() {
+ context = null;
+ });
+
+ it('returns a new sub context', function() {
+ const subContext = createSubContext(context);
+ expect(subContext === context).to.be.false;
+ });
+
+ it('provides access to the parent context properties', function() {
+ const subContext = createSubContext(context);
+ expect(subContext.b).to.equal(42);
+ });
+
+ it('enhances the child context with new properties', function() {
+ const subContext = createSubContext(context, { c: 1 });
+ expect(subContext.b).to.equal(42);
+ expect(subContext.c).to.equal(1);
+ expect(context.c).to.be.undefined;
+ });
+});
diff --git a/packages/melody-runtime/__tests__/FilterSpec.js b/packages/melody-runtime/__tests__/FilterSpec.js
new file mode 100644
index 0000000..5f0bc34
--- /dev/null
+++ b/packages/melody-runtime/__tests__/FilterSpec.js
@@ -0,0 +1,518 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { expect } from 'chai';
+
+import {
+ batch,
+ attrs,
+ styles,
+ classes,
+ merge,
+ replace,
+ reverse,
+ round,
+ title,
+ url_encode,
+ striptags,
+ number_format,
+ format,
+ strtotime,
+} from '../src';
+
+describe('Twig filter runtime', function() {
+ describe('batch filter', function() {
+ it('should group items', function() {
+ const items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'];
+ const expected = [
+ ['a', 'b', 'c'],
+ ['d', 'e', 'f'],
+ ['g', 'h', 'i'],
+ ];
+ const actual = batch(items, 3, 'No Item');
+ expect(actual).to.deep.eql(expected);
+ });
+
+ it('should insert missing if needed', function() {
+ const items = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
+ const expected = [
+ ['a', 'b', 'c'],
+ ['d', 'e', 'f'],
+ ['g', 'No Item', 'No Item'],
+ ];
+ const actual = batch(items, 3, 'No Item');
+ expect(actual).to.deep.eql(expected);
+ });
+ });
+
+ describe('attrs filter', function() {
+ it('should convert map to array', function() {
+ const input = { a: 'b', c: false };
+ const expected = ['a', 'b', 'c', undefined];
+ const actual = attrs(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should keep falsy values that are not false', function() {
+ const input = { a: 'b', c: 0, d: '', e: false };
+ const expected = [
+ 'a',
+ 'b',
+ 'c',
+ undefined,
+ 'd',
+ undefined,
+ 'e',
+ undefined,
+ ];
+ const actual = attrs(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should ignore inherited properties', function() {
+ const input = Object.create({ a: 2 });
+ input.b = 1;
+ const expected = ['b', 1];
+ const actual = attrs(input);
+ expect(actual).to.eql(expected);
+ });
+ });
+
+ describe('styles filter', function() {
+ it('should convert object to string', function() {
+ const input = { declaration: 'value' };
+ const expected = 'declaration:value;';
+ const actual = styles(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should obey conditions for adding other styles', function() {
+ const input = {
+ color: 'red',
+ 'background-image': '',
+ 'background-repeat': false,
+ 'background-attachment': undefined,
+ 'background-position': null,
+ 'background-color': 'blue',
+ border: {},
+ font: [],
+ left: 0,
+ right: NaN,
+ };
+ const expected = 'color:red;background-color:blue;left:0;';
+ const actual = styles(input);
+ expect(actual).to.eql(expected);
+ });
+ });
+
+ describe('classes filter', function() {
+ it('should convert object to string', function() {
+ const input = { class1: true };
+ const expected = 'class1';
+ const actual = classes(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should add base class', function() {
+ const input = { base: 'base-class' };
+ const expected = 'base-class';
+ const actual = classes(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should add multiple base classes', function() {
+ const input = { base: 'base-class base-class1 base-class2' };
+ const expected = 'base-class base-class1 base-class2';
+ const actual = classes(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should add base class and conditional class', function() {
+ const input = { base: 'base-class', class1: true };
+ const expected = 'base-class class1';
+ const actual = classes(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should add base class and obey conditions for adding other classes', function() {
+ const input = { base: 'base-class', class1: true, class2: false };
+ const expected = 'base-class class1';
+ const actual = classes(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should obey conditions for adding other classes', function() {
+ const input = { class1: true, class2: false };
+ const expected = 'class1';
+ const actual = classes(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should add multiple base classes and obey conditions for adding other classes', function() {
+ const input = {
+ base: 'base-class base-class1',
+ class1: true,
+ class2: false,
+ };
+ const expected = 'base-class base-class1 class1';
+ const actual = classes(input);
+ expect(actual).to.eql(expected);
+ });
+ });
+
+ describe('merge filter', function() {
+ it('should merge objects', function() {
+ const inputA = { a: 1 };
+ const inputB = { b: 2 };
+ const expected = { a: 1, b: 2 };
+ const actual = merge(inputA, inputB);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should not mutate its arguments', function() {
+ const inputA = { a: 1 };
+ const inputB = { b: 2 };
+ merge(inputA, inputB);
+ expect(inputA).to.eql({ a: 1 });
+ expect(inputA.b).to.be.undefined;
+ expect(inputB).to.eql({ b: 2 });
+ });
+
+ it('should merge arrays', function() {
+ const inputA = [1, 2];
+ const inputB = [3, 4];
+ const expected = [1, 2, 3, 4];
+ const actual = merge(inputA, inputB);
+ expect(actual).to.eql(expected);
+ });
+ });
+
+ describe('replace filter', function() {
+ it('should replace values within a string', function() {
+ const input = 'hello world!';
+ const expected = 'hello universe!';
+ const actual = replace(input, {
+ world: 'universe',
+ });
+ expect(actual).to.equal(expected);
+ });
+ });
+
+ describe('reverse filter', function() {
+ it('should reverse array elements', function() {
+ const input = [1, 2, 3, 4];
+ const expected = [4, 3, 2, 1];
+ const actual = reverse(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should reverse a string', function() {
+ const input = '1234';
+ const expected = '4321';
+ const actual = reverse(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should not reverse an object', function() {
+ const input = { a: 1, b: 2 };
+ const actual = reverse(input);
+ expect(actual).to.equal(input);
+ });
+ });
+
+ describe('round filter', function() {
+ it('should ceil the number to the given precision', function() {
+ const input = 0.8241331;
+ const expected = 0.9;
+ const actual = round(input, 1, 'ceil');
+ expect(actual).to.eql(expected);
+ });
+
+ it('should floor the number to the given precision', function() {
+ const input = 0.8241331;
+ const expected = 0.8;
+ const actual = round(input, 1, 'floor');
+ expect(actual).to.equal(expected);
+ });
+
+ it('should round the number', function() {
+ const input = 0.8241331;
+ const expected = 1;
+ const actual = round(input, 0, 'common');
+ expect(actual).to.equal(expected);
+ });
+
+ it('should use 0 precision as default', function() {
+ const input = 0.8241331;
+ const expected = 1;
+ const actual = round(input);
+ expect(actual).to.equal(expected);
+ });
+ });
+
+ describe('title filter', function() {
+ it('should title case the first letter of all words', function() {
+ const input = 'hello world is fun!';
+ const expected = 'Hello World Is Fun!';
+ const actual = title(input);
+ expect(actual).to.equal(expected);
+ });
+ });
+
+ describe('url_encode filter', function() {
+ it('should encode a string', function() {
+ const input = 'foo=hello world';
+ const expected = 'foo%3Dhello%20world';
+ const actual = url_encode(input);
+ expect(actual).to.equal(expected);
+ });
+
+ it('should encode an object', function() {
+ const input = { foo: 'hello world', 'foo bar': 2 };
+ const expected = 'foo=hello%20world&foo%20bar=2';
+ const actual = url_encode(input);
+ expect(actual).to.equal(expected);
+ });
+ });
+
+ describe('striptags filter', function() {
+ it('should remove HTML tags', function() {
+ const input = 'test
';
+ const expected = 'test';
+ const actual = striptags(input);
+ expect(actual).to.equal(expected);
+ });
+ });
+
+ describe('number_format filter', function() {
+ it('should format a number', function() {
+ expect(number_format(455121.213, 2, ',', '.')).to.equal(
+ '455.121,21',
+ );
+ });
+
+ it('should use US as default local', function() {
+ expect(number_format(455121.213, 1)).to.equal('455,121.2');
+ });
+
+ it('should extend the number to the precision if needed', function() {
+ expect(number_format(455121, 1)).to.equal('455,121.0');
+ expect(number_format(455121.01, 5)).to.equal('455,121.01000');
+ });
+
+ it('should default to 0 precision', function() {
+ expect(number_format(455121.213)).to.equal('455,121');
+ });
+
+ it('should return 0 on invalid input', function() {
+ expect(number_format('hello', 1)).to.equal('0.0');
+ });
+ });
+
+ describe('format filter', function() {
+ it('should insert strings', function() {
+ expect(format('Hello %s!', 'world')).to.equal('Hello world!');
+ });
+
+ it('should format numbers', function() {
+ expect(format('%01.2f', 123.1)).to.equal('123.10');
+ expect(format('%d', 123456789012345)).to.equal('123456789012345');
+
+ const n = 43951789;
+ const u = -n;
+ const c = 65;
+
+ expect(format("%%b = '%b'", n)).to.equal(
+ "%b = '10100111101010011010101101'",
+ );
+ expect(format("%%c = '%c'", c)).to.equal("%c = 'A'");
+ expect(format("%%d = '%d'", c)).to.equal("%d = '65'");
+ expect(format("%%i = '%i'", c)).to.equal("%i = '65'");
+ expect(format("%%i = '%i'", '')).to.equal("%i = '0'");
+ expect(format("%%e = '%e'", n)).to.equal("%e = '4.395179e+7'");
+ expect(format("%%u = '%u'", n)).to.equal("%u = '43951789'");
+ expect(format("%%u = '%u'", u)).to.equal("%u = '4251015507'");
+ expect(format("%%f = '%f'", n)).to.equal("%f = '43951789.000000'");
+ expect(format("%%f = '%f'", u)).to.equal("%f = '-43951789.000000'");
+ expect(format("%%F = '%F'", n)).to.equal("%F = '43951789.000000'");
+ expect(format("%%g = '%g'", n)).to.equal("%g = '43951789'");
+ expect(format("%%G = '%G'", n)).to.equal("%G = '43951789'");
+ expect(format("%%f = '%.2f'", n)).to.equal("%f = '43951789.00'");
+ expect(format("%%f = '%.*f'", 1, n)).to.equal("%f = '43951789.0'");
+ expect(format("%%o = '%o'", n)).to.equal("%o = '247523255'");
+ expect(format("%%s = '%s'", n)).to.equal("%s = '43951789'");
+ expect(format("%%x = '%x'", n)).to.equal("%x = '29ea6ad'");
+ expect(format("%%X = '%X'", n)).to.equal("%X = '29EA6AD'");
+ expect(format("%%+d = '%+d'", n)).to.equal("%+d = '+43951789'");
+ expect(format("%%+d = '% d'", n)).to.equal("%+d = ' 43951789'");
+ expect(format("%%+d = '%+d'", u)).to.equal("%+d = '-43951789'");
+
+ expect(() => format("%%f = '%*.*f'", 1, n)).to.throw(
+ 'toFixed() digits argument must be between 0 and 20',
+ );
+ });
+
+ it('should add padding', function() {
+ expect(format('[%10s]', 'monkey')).to.equal('[ monkey]');
+ expect(format('[%*s]', 10, 'monkey')).to.equal('[ monkey]');
+ expect(format('[%*s]', -10, 'monkey')).to.equal('[monkey ]');
+ expect(format('[%-10s]', 'monkey')).to.equal('[monkey ]');
+ expect(format('[%10.10s]', 'many monkeys')).to.equal(
+ '[many monke]',
+ );
+ expect(format("[%'#10s]", 'monkey')).to.equal('[####monkey]');
+ expect(format('%-03s', 'E')).to.equal('E00');
+
+ expect(() => format('[%*s]', 'fun', 'monkey')).to.throw(
+ 'sprintf: (minimum-)width must be finite',
+ );
+ });
+
+ it('should swap arguments', function() {
+ expect(format('%2$s %1$s!', 'world', 'Hello')).to.equal(
+ 'Hello world!',
+ );
+ expect(
+ format('The %2$s contains %1$04d monkeys', 12000, 'zoo'),
+ ).to.equal('The zoo contains 12000 monkeys');
+ expect(
+ format('The %2$s contains %1$04d monkeys', 120, 'zoo'),
+ ).to.equal('The zoo contains 0120 monkeys');
+ });
+
+ it('should ignore invalid options', function() {
+ expect(format('Hello %a!', 'world')).to.equal('Hello %a!');
+ });
+ });
+
+ describe('strtotime filter', function() {
+ const now = 1129633200;
+ it('should convert relative timestamps', function() {
+ expect(strtotime('+1 day', now)).to.equal(1129719600);
+ expect(strtotime('+1 week 2 days 4 hours 2 seconds', now)).to.equal(
+ 1130425202,
+ );
+ expect(
+ strtotime('+1 day 1 week 2 days 4 hours 2 seconds', now),
+ ).to.equal(1130511602);
+ expect(strtotime('last month', now)).to.equal(1127041200);
+ expect(strtotime('next Thursday', now)).to.equal(1129806000);
+ expect(strtotime('last Monday', now)).to.equal(1129546800);
+ expect(strtotime('now', now)).to.equal(now);
+
+ // expect(strtotime('12.06.45', now)).to.equal(1467194805);
+ // expect(strtotime('12:06:45', now)).to.equal(1467194805);
+ });
+
+ it('should convert dates after 2000', function() {
+ expect(strtotime('10 September 09')).to.be.within(
+ 1252533600 - 10000,
+ 1252533600 + 10000,
+ );
+ expect(strtotime('10 September 2009')).to.be.within(
+ 1252533600 - 10000,
+ 1252533600 + 10000,
+ );
+ expect(strtotime('2009-09-10', now)).to.be.within(
+ 1252533600 - 10000,
+ 1252533600 + 10000,
+ );
+ expect(strtotime('2009/09/10', now)).to.be.within(
+ 1252533600 - 10000,
+ 1252533600 + 10000,
+ );
+ expect(strtotime('10-09-2009', now)).to.be.within(
+ 1252533600 - 10000,
+ 1252533600 + 10000,
+ );
+ expect(strtotime('10.09.2009', now)).to.be.within(
+ 1252533600 - 10000,
+ 1252533600 + 10000,
+ );
+ expect(strtotime('09/10/2009', now)).to.be.within(
+ 1252533600 - 10000,
+ 1252533600 + 10000,
+ );
+ expect(strtotime('09-09-10', now)).to.be.within(
+ 1252533600 - 10000,
+ 1252533600 + 10000,
+ );
+ expect(strtotime('09/10/09', now)).to.be.within(
+ 1252533600 - 10000,
+ 1252533600 + 10000,
+ );
+ });
+
+ it('should convert dates before 2000', function() {
+ expect(strtotime('10 September 79', now)).to.be.within(
+ 305766000 - 10000,
+ 305766000 + 10000,
+ );
+ expect(strtotime('10 September 1979', now)).to.within(
+ 305766000 - 10000,
+ 305766000 + 10000,
+ );
+ expect(strtotime('1979-09-10', now)).to.be.within(
+ 305766000 - 10000,
+ 305766000 + 10000,
+ );
+ expect(strtotime('1979/09/10', now)).to.be.within(
+ 305766000 - 10000,
+ 305766000 + 10000,
+ );
+ expect(strtotime('10-09-1979', now)).to.be.within(
+ 305766000 - 10000,
+ 305766000 + 10000,
+ );
+ expect(strtotime('10.09.1979', now)).to.be.within(
+ 305766000 - 10000,
+ 305766000 + 10000,
+ );
+ expect(strtotime('09/10/1979', now)).to.be.within(
+ 305766000 - 10000,
+ 305766000 + 10000,
+ );
+ expect(strtotime('79-09-10', now)).to.be.within(
+ 305766000 - 10000,
+ 305766000 + 10000,
+ );
+ expect(strtotime('10.09.79', now)).to.be.within(
+ 305766000 - 10000,
+ 305766000 + 10000,
+ );
+ expect(strtotime('09/10/79', now)).to.be.within(
+ 305766000 - 10000,
+ 305766000 + 10000,
+ );
+ });
+
+ it('should convert timestamps', function() {
+ expect(strtotime('2009-05-04 08:30:00 GMT')).to.equal(1241425800);
+ expect(strtotime('2009-05-04 08:30:00+00')).to.equal(1241425800);
+ expect(strtotime('2009-05-04 08:30:00+02:00')).to.equal(1241418600);
+ expect(strtotime('2009-05-04 08:30:00z')).to.equal(1241425800);
+ });
+
+ it('should return false when given an empty string', function() {
+ expect(strtotime('')).to.equal(false);
+ });
+
+ it('should return false when given invalid input', function() {
+ expect(strtotime('1801/09/10', now)).to.equal(false);
+ expect(strtotime('1979-13-10', now)).to.equal(false);
+ expect(strtotime('1979/13/10', now)).to.equal(false);
+ });
+ });
+});
diff --git a/packages/melody-runtime/__tests__/FunctionsSpec.js b/packages/melody-runtime/__tests__/FunctionsSpec.js
new file mode 100644
index 0000000..d118460
--- /dev/null
+++ b/packages/melody-runtime/__tests__/FunctionsSpec.js
@@ -0,0 +1,129 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { expect } from 'chai';
+
+import { random, min, max, cycle, attribute } from '../src/functions';
+
+describe('Twig function runtime', function() {
+ describe('random', function() {
+ it('should accept an upper bound', function() {
+ const input = 0;
+ const expected = 0;
+ const actual = random(input);
+ expect(actual).to.eql(expected);
+ });
+
+ it('should select an element from an array', function() {
+ const input = ['a', 'b'];
+ const actual = random(input);
+ expect(actual).to.be.oneOf(input);
+ });
+ });
+
+ describe('min', function() {
+ it('should accept varargs', function() {
+ const actual = min(1, 2, 3, 4, 5, 6, 7);
+ expect(actual).to.equal(1);
+ });
+
+ it('should accept an array', function() {
+ const input = [1, 2, 3, 4, 5, 6, 7];
+ const actual = min(input);
+ expect(actual).to.equal(1);
+ });
+
+ it('should select the lowest value in an object', function() {
+ const input = { a: 1, b: 2 };
+ const actual = min(input);
+ expect(actual).to.equal(1);
+ });
+ });
+
+ describe('max', function() {
+ it('should accept varargs', function() {
+ const actual = max(1, 2, 3, 4, 5, 6, 7);
+ expect(actual).to.equal(7);
+ });
+
+ it('should accept an array', function() {
+ const input = [1, 2, 3, 4, 5, 6, 7];
+ const actual = max(input);
+ expect(actual).to.equal(7);
+ });
+
+ it('should select the lowest value in an object', function() {
+ const input = { a: 1, b: 2 };
+ const actual = max(input);
+ expect(actual).to.equal(2);
+ });
+ });
+
+ describe('cycle', function() {
+ it('should return the element at the given index', function() {
+ const input = ['a', 'b', 'c'];
+ expect(cycle(input, 0)).to.equal('a');
+ expect(cycle(input, 1)).to.equal('b');
+ expect(cycle(input, 2)).to.equal('c');
+ expect(cycle(input, 3)).to.equal('a');
+ expect(cycle(input, 4)).to.equal('b');
+ expect(cycle(input, 5)).to.equal('c');
+ expect(cycle(input, 6)).to.equal('a');
+ });
+ });
+
+ describe('attribute', function() {
+ it('should return an array index', function() {
+ expect(attribute(['a', 'b', 'c'], 1)).to.equal('b');
+ });
+
+ it('should return the value of a property', function() {
+ const input = { a: 42 };
+ expect(attribute(input, 'a')).to.equal(42);
+ });
+
+ it('should evaluate a function, forwarding the arguments', function() {
+ const input = {
+ a(b, c) {
+ return b + c;
+ },
+ };
+ expect(attribute(input, 'a', [2, 3])).to.equal(5);
+ });
+
+ it('should use a getter if available', function() {
+ const input = {
+ getAValue() {
+ return 5;
+ },
+ };
+ expect(attribute(input, 'aValue')).to.equal(5);
+ });
+
+ it('should use an is property if available', function() {
+ const input = {
+ isAValue() {
+ return true;
+ },
+ };
+ expect(attribute(input, 'aValue')).to.equal(true);
+ });
+
+ it('should return undefined on mismatch', function() {
+ const input = {};
+ expect(attribute(input, 'toString')).to.equal(undefined);
+ });
+ });
+});
diff --git a/packages/melody-runtime/__tests__/HelperSpec.js b/packages/melody-runtime/__tests__/HelperSpec.js
new file mode 100644
index 0000000..c4b095d
--- /dev/null
+++ b/packages/melody-runtime/__tests__/HelperSpec.js
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { assert } from 'chai';
+import { isEmpty, inheritBlocks } from '../src/helpers';
+
+describe('Runtime/Helpers', function() {
+ describe('isEmpty', function() {
+ it('should return true for an empty array', function() {
+ assert.equal(isEmpty([]), true);
+ });
+
+ it('should return false for a non-empty array', function() {
+ assert.equal(isEmpty([42]), false);
+ });
+
+ it('should return true for null', function() {
+ assert.equal(isEmpty(null), true);
+ });
+
+ it('should return true for undefined', function() {
+ assert.equal(isEmpty(undefined), true);
+ });
+
+ it('should return false for an empty object', function() {
+ assert.equal(isEmpty({}), false);
+ });
+
+ it('should return false for 0', function() {
+ assert.equal(isEmpty(0), false);
+ });
+ });
+
+ describe('inheritBlocks', function() {
+ it('copies block rendering methods', function() {
+ const template = { render: 1 };
+ const used = { renderFoo: 2, render: 3 };
+ inheritBlocks(template, used);
+ assert.equal(template.renderFoo, 2);
+ assert.equal(template.render, 1);
+ });
+
+ it('copies block rendering methods with mappings', function() {
+ const template = { render: 1 };
+ const used = { renderFoo: 2, render: 3, renderFoobar: 4 };
+ const mapping = { renderFoo: 'renderBar' };
+ inheritBlocks(template, used, mapping);
+ assert.equal(template.renderFoo, undefined);
+ assert.equal(template.renderFoobar, undefined);
+ assert.equal(template.renderBar, 2);
+ assert.equal(template.render, 1);
+ });
+
+ it('ignores prototype methods', function() {
+ const bak = Object.prototype.fooBar;
+ Object.prototype.fooBar = 1;
+
+ const template = Object.create(null);
+ const used = { renderFoo: 2, render: 3 };
+ inheritBlocks(template, used);
+
+ assert.equal(template.renderFoo, 2);
+ assert.equal(template.fooBar, undefined);
+
+ Object.prototype.fooBar = bak;
+ });
+ });
+});
diff --git a/packages/melody-runtime/package.json b/packages/melody-runtime/package.json
new file mode 100644
index 0000000..0b8084c
--- /dev/null
+++ b/packages/melody-runtime/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "melody-runtime",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "lodash": "^4.12.0"
+ },
+ "devDependencies": {
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-runtime/src/context.js b/packages/melody-runtime/src/context.js
new file mode 100644
index 0000000..4606af7
--- /dev/null
+++ b/packages/melody-runtime/src/context.js
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Creates a subcontext for a given context and assigns custom values to it.
+ * @param parent The parent context
+ * @param customValues Any custom values that should be assigned to the child context
+ * @returns A new child context
+ */
+export function createSubContext(
+ parent: Object,
+ customValues?: Object,
+): Object {
+ const subContext = Object.create(parent);
+ if (customValues) {
+ Object.assign(subContext, customValues);
+ }
+ return subContext;
+}
diff --git a/packages/melody-runtime/src/filters.js b/packages/melody-runtime/src/filters.js
new file mode 100644
index 0000000..df14e69
--- /dev/null
+++ b/packages/melody-runtime/src/filters.js
@@ -0,0 +1,963 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { round as lRound, ceil, floor, isString } from 'lodash';
+
+export function batch(items: Array, groupSize: Number, missing = '') {
+ const batchedResult = [],
+ len = items.length;
+ for (let i = 0; i < len; i += groupSize) {
+ const batch = [];
+ for (let j = 0; j < groupSize; j++) {
+ batch[batch.length] = i + j < len ? items[i + j] : missing;
+ }
+ batchedResult[batchedResult.length] = batch;
+ }
+ return batchedResult;
+}
+
+export function attrs(attrMap: Object) {
+ const attrArray = [];
+ for (const attr in attrMap) {
+ if (!attrMap.hasOwnProperty(attr)) {
+ continue;
+ }
+
+ const value = attrMap[attr];
+ attrArray.push(
+ attr,
+ value === false || value === null || value === 0 || value === ''
+ ? undefined
+ : value,
+ );
+ }
+ return attrArray;
+}
+
+export function styles(declarationsMap) {
+ let declarations = '';
+ const keys = Object.keys(declarationsMap);
+ for (let i = 0; i < keys.length; i++) {
+ const value = declarationsMap[keys[i]];
+ if (
+ (typeof value === 'string' && value !== '') ||
+ (typeof value === 'number' && !isNaN(value))
+ ) {
+ declarations += `${keys[i]}:${value};`;
+ }
+ }
+ return declarations;
+}
+
+export function classes(classMap: Object) {
+ const classArray = [];
+ for (const cla in classMap) {
+ if (!classMap.hasOwnProperty(cla)) {
+ continue;
+ }
+ if (cla === 'base') {
+ classArray.push(classMap[cla]);
+ } else if (classMap[cla]) {
+ classArray.push(cla);
+ }
+ }
+ return classArray.join(' ');
+}
+
+export function merge(initial, additions) {
+ if (Array.isArray(initial) && Array.isArray(additions)) {
+ return [...initial, ...additions];
+ }
+ return Object.assign({}, initial, additions);
+}
+
+export function replace(input, replacements) {
+ let result = input;
+ for (const search in replacements) {
+ if (replacements.hasOwnProperty(search)) {
+ result = result.replace(search, replacements[search]);
+ }
+ }
+ return result;
+}
+
+export function reverse(iterable) {
+ if (Array.isArray(iterable)) {
+ const resArray = [];
+ for (let i = iterable.length - 1; i >= 0; i--) {
+ resArray[resArray.length] = iterable[i];
+ }
+ return resArray;
+ } else if (isString(iterable)) {
+ let resString = '';
+ for (let i = iterable.length - 1; i >= 0; i--) {
+ resString += iterable[i];
+ }
+ return resString;
+ } else {
+ // JavaScript isn't PHP. The order in which properties are added to
+ // an object may or may not be relevant when iterating over the values in
+ // an object.
+ // Thus, we don't support that kind of usage.
+ return iterable;
+ }
+}
+
+export function round(num: Number, precision: Number = 0, method = 'common') {
+ switch (method) {
+ case 'ceil':
+ return ceil(num, precision);
+ case 'floor':
+ return floor(num, precision);
+ case 'common':
+ default:
+ return lRound(num, precision);
+ }
+}
+
+export function striptags(input) {
+ // discuss at: http://phpjs.org/functions/strip_tags/
+ // original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // improved by: Luke Godfrey
+ // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // input by: Pul
+ // input by: Alex
+ // input by: Marc Palau
+ // input by: Brett Zamir (http://brett-zamir.me)
+ // input by: Bobby Drake
+ // input by: Evertjan Garretsen
+ // bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // bugfixed by: Onno Marsman
+ // bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // bugfixed by: Eric Nagel
+ // bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // bugfixed by: Tomasz Wesolowski
+ // revised by: Rafał Kukawski (http://blog.kukawski.pl/)
+ // example 1: strip_tags('Kevin
van Zonneveld ', '');
+ // returns 1: 'Kevin van Zonneveld '
+ // example 2: strip_tags(' Kevin van Zonneveld
', '');
+ // returns 2: '
Kevin van Zonneveld
'
+ // example 3: strip_tags("Kevin van Zonneveld ", "");
+ // returns 3: " Kevin van Zonneveld "
+ // example 4: strip_tags('1 < 5 5 > 1');
+ // returns 4: '1 < 5 5 > 1'
+ // example 5: strip_tags('1 1');
+ // returns 5: '1 1'
+ // example 6: strip_tags('1 1', ' ');
+ // returns 6: '1 1'
+ // example 7: strip_tags('1 1', ' ');
+ // returns 7: '1 1'
+ var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi,
+ commentsAndPhpTags = /|<\?(?:php)?[\s\S]*?\?>/gi;
+ return input.replace(commentsAndPhpTags, '').replace(tags, '');
+}
+
+function toFixedFix(n, prec) {
+ var k = Math.pow(10, prec);
+ return '' + (Math.round(n * k) / k).toFixed(prec);
+}
+
+/**
+ * Filters: number_format
+ * @param number
+ * @param decimals
+ * @param dec_point
+ * @param thousands_sep
+ * @returns {string}
+ */
+export function number_format(numberParam, decimals, dec_point, thousands_sep) {
+ // discuss at: http://phpjs.org/functions/number_format/
+ // original by: Jonas Raoni Soares Silva (http://www.jsfromhell.com)
+ // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // improved by: davook
+ // improved by: Brett Zamir (http://brett-zamir.me)
+ // improved by: Brett Zamir (http://brett-zamir.me)
+ // improved by: Theriault
+ // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // bugfixed by: Michael White (http://getsprink.com)
+ // bugfixed by: Benjamin Lupton
+ // bugfixed by: Allan Jensen (http://www.winternet.no)
+ // bugfixed by: Howard Yeend
+ // bugfixed by: Diogo Resende
+ // bugfixed by: Rival
+ // bugfixed by: Brett Zamir (http://brett-zamir.me)
+ // revised by: Jonas Raoni Soares Silva (http://www.jsfromhell.com)
+ // revised by: Luke Smith (http://lucassmith.name)
+ // input by: Kheang Hok Chin (http://www.distantia.ca/)
+ // input by: Jay Klehr
+ // input by: Amir Habibi (http://www.residence-mixte.com/)
+ // input by: Amirouche
+
+ const number = (numberParam + '').replace(/[^0-9+\-Ee.]/g, '');
+ var n = !isFinite(+number) ? 0 : +number,
+ prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
+ sep = typeof thousands_sep === 'undefined' ? ',' : thousands_sep,
+ dec = typeof dec_point === 'undefined' ? '.' : dec_point,
+ s;
+ // Fix for IE parseFloat(0.55).toFixed(0) = 0;
+ s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
+ if (s[0].length > 3) {
+ s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep);
+ }
+ if ((s[1] || '').length < prec) {
+ s[1] = s[1] || '';
+ s[1] += new Array(prec - s[1].length + 1).join('0');
+ }
+ return s.join(dec);
+}
+
+export function format(...a) {
+ // discuss at: http://phpjs.org/functions/sprintf/
+ // original by: Ash Searle (http://hexmen.com/blog/)
+ // improved by: Michael White (http://getsprink.com)
+ // improved by: Jack
+ // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // improved by: Dj
+ // improved by: Allidylls
+ // input by: Paulo Freitas
+ // input by: Brett Zamir (http://brett-zamir.me)
+ // example 1: sprintf("%01.2f", 123.1);
+ // returns 1: 123.10
+ // example 2: sprintf("[%10s]", 'monkey');
+ // returns 2: '[ monkey]'
+ // example 3: sprintf("[%'#10s]", 'monkey');
+ // returns 3: '[####monkey]'
+ // example 4: sprintf("%d", 123456789012345);
+ // returns 4: '123456789012345'
+ // example 5: sprintf('%-03s', 'E');
+ // returns 5: 'E00'
+
+ var regex = /%%|%(\d+\$)?([\-+\'#0 ]*)(\*\d+\$|\*|\d+)?(?:\.(\*\d+\$|\*|\d+))?([scboxXuideEfFgG])/g;
+ var i = 0;
+ var format = a[i++];
+
+ // pad()
+ var pad = function(str, len, chr = ' ', leftJustify) {
+ var padding =
+ str.length >= len
+ ? ''
+ : new Array((1 + len - str.length) >>> 0).join(chr);
+ return leftJustify ? str + padding : padding + str;
+ };
+
+ // justify()
+ var justify = function(
+ value,
+ prefix,
+ leftJustify,
+ minWidth,
+ zeroPad,
+ customPadChar,
+ ) {
+ var diff = minWidth - value.length;
+ if (diff > 0) {
+ if (leftJustify || !zeroPad) {
+ return pad(value, minWidth, customPadChar, leftJustify);
+ } else {
+ return (
+ value.slice(0, prefix.length) +
+ pad('', diff, '0', true) +
+ value.slice(prefix.length)
+ );
+ }
+ }
+ return value;
+ };
+
+ // formatBaseX()
+ var formatBaseX = function(
+ n,
+ base,
+ prefixParam,
+ leftJustify,
+ minWidth,
+ precision,
+ zeroPad,
+ ) {
+ // Note: casts negative numbers to positive ones
+ var number = n >>> 0;
+ const prefix =
+ (prefixParam &&
+ number &&
+ {
+ '2': '0b',
+ '8': '0',
+ '16': '0x',
+ }[base]) ||
+ '';
+ const value =
+ prefix + pad(number.toString(base), precision || 0, '0', false);
+ return justify(value, prefix, leftJustify, minWidth, zeroPad);
+ };
+
+ // formatString()
+ var formatString = function(
+ valueParam,
+ leftJustify,
+ minWidth,
+ precision,
+ zeroPad,
+ customPadChar,
+ ) {
+ let value = valueParam;
+ if (precision !== null && precision !== undefined) {
+ value = value.slice(0, precision);
+ }
+ return justify(
+ value,
+ '',
+ leftJustify,
+ minWidth,
+ zeroPad,
+ customPadChar,
+ );
+ };
+
+ // doFormat()
+ var doFormat = function(
+ substring,
+ valueIndex,
+ flags,
+ minWidthParam,
+ precisionParam,
+ type,
+ ) {
+ var number, prefix, method, textTransform, value;
+
+ if (substring === '%%') {
+ return '%';
+ }
+
+ // parse flags
+ var leftJustify = false;
+ var positivePrefix = '';
+ var zeroPad = false;
+ var prefixBaseX = false;
+ var customPadChar = ' ';
+ var flagsl = flags.length;
+ var j;
+ for (j = 0; flags && j < flagsl; j++) {
+ switch (flags.charAt(j)) {
+ case ' ':
+ positivePrefix = ' ';
+ break;
+ case '+':
+ positivePrefix = '+';
+ break;
+ case '-':
+ leftJustify = true;
+ break;
+ case "'":
+ customPadChar = flags.charAt(j + 1);
+ break;
+ case '0':
+ zeroPad = true;
+ customPadChar = '0';
+ break;
+ case '#':
+ prefixBaseX = true;
+ break;
+ }
+ }
+
+ // parameters may be null, undefined, empty-string or real valued
+ // we want to ignore null, undefined and empty-string values
+ let minWidth = minWidthParam;
+ if (!minWidth) {
+ minWidth = 0;
+ } else if (minWidth === '*') {
+ minWidth = +a[i++];
+ } else if (minWidth.charAt(0) === '*') {
+ minWidth = +a[minWidth.slice(1, -1)];
+ } else {
+ minWidth = +minWidth;
+ }
+
+ // Note: undocumented perl feature:
+ if (minWidth < 0) {
+ minWidth = -minWidth;
+ leftJustify = true;
+ }
+
+ if (!isFinite(minWidth)) {
+ throw new Error('sprintf: (minimum-)width must be finite');
+ }
+
+ let precision = precisionParam;
+ if (!precision) {
+ precision =
+ 'fFeE'.indexOf(type) > -1 ? 6 : type === 'd' ? 0 : undefined;
+ } else if (precision === '*') {
+ precision = +a[i++];
+ } else if (precision.charAt(0) === '*') {
+ precision = +a[precision.slice(1, -1)];
+ } else {
+ precision = +precision;
+ }
+
+ // grab value using valueIndex if required?
+ value = valueIndex ? a[valueIndex.slice(0, -1)] : a[i++];
+
+ switch (type) {
+ case 's':
+ return formatString(
+ String(value),
+ leftJustify,
+ minWidth,
+ precision,
+ zeroPad,
+ customPadChar,
+ );
+ case 'c':
+ return formatString(
+ String.fromCharCode(+value),
+ leftJustify,
+ minWidth,
+ precision,
+ zeroPad,
+ );
+ case 'b':
+ return formatBaseX(
+ value,
+ 2,
+ prefixBaseX,
+ leftJustify,
+ minWidth,
+ precision,
+ zeroPad,
+ );
+ case 'o':
+ return formatBaseX(
+ value,
+ 8,
+ prefixBaseX,
+ leftJustify,
+ minWidth,
+ precision,
+ zeroPad,
+ );
+ case 'x':
+ return formatBaseX(
+ value,
+ 16,
+ prefixBaseX,
+ leftJustify,
+ minWidth,
+ precision,
+ zeroPad,
+ );
+ case 'X':
+ return formatBaseX(
+ value,
+ 16,
+ prefixBaseX,
+ leftJustify,
+ minWidth,
+ precision,
+ zeroPad,
+ ).toUpperCase();
+ case 'u':
+ return formatBaseX(
+ value,
+ 10,
+ prefixBaseX,
+ leftJustify,
+ minWidth,
+ precision,
+ zeroPad,
+ );
+ case 'i':
+ case 'd':
+ number = +value || 0;
+ // Plain Math.round doesn't just truncate
+ number = Math.round(number - number % 1);
+ prefix = number < 0 ? '-' : positivePrefix;
+ value =
+ prefix +
+ pad(String(Math.abs(number)), precision, '0', false);
+ return justify(value, prefix, leftJustify, minWidth, zeroPad);
+ case 'e':
+ case 'E':
+ case 'f': // Should handle locales (as per setlocale)
+ case 'F':
+ case 'g':
+ case 'G':
+ number = +value;
+ prefix = number < 0 ? '-' : positivePrefix;
+ method = ['toExponential', 'toFixed', 'toPrecision'][
+ 'efg'.indexOf(type.toLowerCase())
+ ];
+ textTransform = ['toString', 'toUpperCase'][
+ 'eEfFgG'.indexOf(type) % 2
+ ];
+ value = prefix + Math.abs(number)[method](precision);
+ return justify(value, prefix, leftJustify, minWidth, zeroPad)[
+ textTransform
+ ]();
+ default:
+ return substring;
+ }
+ };
+
+ return format.replace(regex, doFormat);
+}
+
+export function title(input) {
+ let res = '',
+ ucNext = true;
+ for (let i = 0, len = input.length; i < len; i++) {
+ const c = input[i];
+ if (c === '\n' || c === ' ' || c === '\t') {
+ ucNext = true;
+ res += c;
+ } else if (ucNext) {
+ res += c.toUpperCase();
+ ucNext = false;
+ } else {
+ res += c;
+ }
+ }
+ return res;
+}
+
+export function url_encode(input) {
+ if (isString(input)) {
+ return encodeURIComponent(input);
+ }
+ const parts = [];
+ for (const key in input) {
+ if (input.hasOwnProperty(key)) {
+ parts.push(
+ encodeURIComponent(key) + '=' + encodeURIComponent(input[key]),
+ );
+ }
+ }
+ return parts.join('&');
+}
+
+export function strtotime(input, now) {
+ // discuss at: http://phpjs.org/functions/strtotime/
+ // version: 1109.2016
+ // original by: Caio Ariede (http://caioariede.com)
+ // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+ // improved by: Caio Ariede (http://caioariede.com)
+ // improved by: A. Matías Quezada (http://amatiasq.com)
+ // improved by: preuter
+ // improved by: Brett Zamir (http://brett-zamir.me)
+ // improved by: Mirko Faber
+ // input by: David
+ // bugfixed by: Wagner B. Soares
+ // bugfixed by: Artur Tchernychev
+ // bugfixed by: Stephan Bösch-Plepelits (http://github.com/plepe)
+ // note: Examples all have a fixed timestamp to prevent tests to fail because of variable time(zones)
+ // example 1: strtotime('+1 day', 1129633200);
+ // returns 1: 1129719600
+ // example 2: strtotime('+1 week 2 days 4 hours 2 seconds', 1129633200);
+ // returns 2: 1130425202
+ // example 3: strtotime('last month', 1129633200);
+ // returns 3: 1127041200
+ // example 4: strtotime('2009-05-04 08:30:00 GMT');
+ // returns 4: 1241425800
+ // example 5: strtotime('2009-05-04 08:30:00+00');
+ // returns 5: 1241425800
+ // example 6: strtotime('2009-05-04 08:30:00+02:00');
+ // returns 6: 1241418600
+ // example 7: strtotime('2009-05-04T08:30:00Z');
+ // returns 7: 1241425800
+
+ var parsed,
+ match,
+ today,
+ year,
+ date,
+ days,
+ ranges,
+ len,
+ times,
+ regex,
+ i,
+ fail = false;
+
+ if (!input) {
+ return fail;
+ }
+
+ // Unecessary spaces
+ const text = input
+ .replace(/^\s+|\s+$/g, '')
+ .replace(/\s{2,}/g, ' ')
+ .replace(/[\t\r\n]/g, '')
+ .toLowerCase();
+
+ // in contrast to php, js Date.parse function interprets:
+ // dates given as yyyy-mm-dd as in timezone: UTC,
+ // dates with "." or "-" as MDY instead of DMY
+ // dates with two-digit years differently
+ // etc...etc...
+ // ...therefore we manually parse lots of common date formats
+ match = text.match(
+ /^(\d{1,4})([\-\.\/\:])(\d{1,2})([\-\.\/\:])(\d{1,4})(?:\s(\d{1,2}):(\d{2})?:?(\d{2})?)?(?:\s([A-Z]+)?)?$/,
+ );
+
+ if (match && match[2] === match[4]) {
+ if (match[1] > 1901) {
+ switch (match[2]) {
+ case '-': {
+ // YYYY-M-D
+ if (match[3] > 12 || match[5] > 31) {
+ return fail;
+ }
+
+ return (
+ new Date(
+ match[1],
+ parseInt(match[3], 10) - 1,
+ match[5],
+ match[6] || 0,
+ match[7] || 0,
+ match[8] || 0,
+ match[9] || 0,
+ ) / 1000
+ );
+ }
+ case '.': {
+ // YYYY.M.D is not parsed by strtotime()
+ return fail;
+ }
+ case '/': {
+ // YYYY/M/D
+ if (match[3] > 12 || match[5] > 31) {
+ return fail;
+ }
+
+ return (
+ new Date(
+ match[1],
+ parseInt(match[3], 10) - 1,
+ match[5],
+ match[6] || 0,
+ match[7] || 0,
+ match[8] || 0,
+ match[9] || 0,
+ ) / 1000
+ );
+ }
+ }
+ } else if (match[5] > 1901) {
+ switch (match[2]) {
+ case '-': {
+ // D-M-YYYY
+ if (match[3] > 12 || match[1] > 31) {
+ return fail;
+ }
+
+ return (
+ new Date(
+ match[5],
+ parseInt(match[3], 10) - 1,
+ match[1],
+ match[6] || 0,
+ match[7] || 0,
+ match[8] || 0,
+ match[9] || 0,
+ ) / 1000
+ );
+ }
+ case '.': {
+ // D.M.YYYY
+ if (match[3] > 12 || match[1] > 31) {
+ return fail;
+ }
+
+ return (
+ new Date(
+ match[5],
+ parseInt(match[3], 10) - 1,
+ match[1],
+ match[6] || 0,
+ match[7] || 0,
+ match[8] || 0,
+ match[9] || 0,
+ ) / 1000
+ );
+ }
+ case '/': {
+ // M/D/YYYY
+ if (match[1] > 12 || match[3] > 31) {
+ return fail;
+ }
+
+ return (
+ new Date(
+ match[5],
+ parseInt(match[1], 10) - 1,
+ match[3],
+ match[6] || 0,
+ match[7] || 0,
+ match[8] || 0,
+ match[9] || 0,
+ ) / 1000
+ );
+ }
+ }
+ } else {
+ switch (match[2]) {
+ case '-': {
+ // YY-M-D
+ if (
+ match[3] > 12 ||
+ match[5] > 31 ||
+ (match[1] < 70 && match[1] > 38)
+ ) {
+ return fail;
+ }
+
+ year =
+ match[1] >= 0 && match[1] <= 38
+ ? +match[1] + 2000
+ : match[1];
+ return (
+ new Date(
+ year,
+ parseInt(match[3], 10) - 1,
+ match[5],
+ match[6] || 0,
+ match[7] || 0,
+ match[8] || 0,
+ match[9] || 0,
+ ) / 1000
+ );
+ }
+ case '.': {
+ // D.M.YY or H.MM.SS
+ if (match[5] >= 70) {
+ // D.M.YY
+ if (match[3] > 12 || match[1] > 31) {
+ return fail;
+ }
+
+ return (
+ new Date(
+ match[5],
+ parseInt(match[3], 10) - 1,
+ match[1],
+ match[6] || 0,
+ match[7] || 0,
+ match[8] || 0,
+ match[9] || 0,
+ ) / 1000
+ );
+ }
+ if (match[5] < 60 && !match[6]) {
+ // H.MM.SS
+ if (match[1] > 23 || match[3] > 59) {
+ return fail;
+ }
+
+ today = new Date();
+ return (
+ new Date(
+ today.getFullYear(),
+ today.getMonth(),
+ today.getDate(),
+ match[1] || 0,
+ match[3] || 0,
+ match[5] || 0,
+ match[9] || 0,
+ ) / 1000
+ );
+ }
+
+ // invalid format, cannot be parsed
+ return fail;
+ }
+ case '/': {
+ // M/D/YY
+ if (
+ match[1] > 12 ||
+ match[3] > 31 ||
+ (match[5] < 70 && match[5] > 38)
+ ) {
+ return fail;
+ }
+
+ year =
+ match[5] >= 0 && match[5] <= 38
+ ? +match[5] + 2000
+ : match[5];
+ return (
+ new Date(
+ year,
+ parseInt(match[1], 10) - 1,
+ match[3],
+ match[6] || 0,
+ match[7] || 0,
+ match[8] || 0,
+ match[9] || 0,
+ ) / 1000
+ );
+ }
+ case ':': {
+ // HH:MM:SS
+ if (match[1] > 23 || match[3] > 59 || match[5] > 59) {
+ return fail;
+ }
+
+ today = new Date();
+ return (
+ new Date(
+ today.getFullYear(),
+ today.getMonth(),
+ today.getDate(),
+ match[1] || 0,
+ match[3] || 0,
+ match[5] || 0,
+ ) / 1000
+ );
+ }
+ }
+ }
+ }
+
+ // other formats and "now" should be parsed by Date.parse()
+ if (text === 'now') {
+ return now === null || isNaN(now)
+ ? (new Date().getTime() / 1000) | 0
+ : now | 0;
+ }
+ if (!isNaN((parsed = Date.parse(text)))) {
+ return (parsed / 1000) | 0;
+ }
+ // Browsers != Chrome have problems parsing ISO 8601 date strings, as they do
+ // not accept lower case characters, space, or shortened time zones.
+ // Therefore, fix these problems and try again.
+ // Examples:
+ // 2015-04-15 20:33:59+02
+ // 2015-04-15 20:33:59z
+ // 2015-04-15t20:33:59+02:00
+ match = text.match(
+ /^([0-9]{4}-[0-9]{2}-[0-9]{2})[ t]([0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?)([\+-][0-9]{2}(:[0-9]{2})?|z)/,
+ );
+ if (match) {
+ // fix time zone information
+ if (match[4] == 'z') {
+ match[4] = 'Z';
+ } else if (match[4].match(/^([\+-][0-9]{2})$/)) {
+ match[4] = match[4] + ':00';
+ }
+
+ if (
+ !isNaN((parsed = Date.parse(match[1] + 'T' + match[2] + match[4])))
+ ) {
+ return (parsed / 1000) | 0;
+ }
+ }
+
+ date = now ? new Date(now * 1000) : new Date();
+ days = {
+ sun: 0,
+ mon: 1,
+ tue: 2,
+ wed: 3,
+ thu: 4,
+ fri: 5,
+ sat: 6,
+ };
+ ranges = {
+ yea: 'FullYear',
+ mon: 'Month',
+ day: 'Date',
+ hou: 'Hours',
+ min: 'Minutes',
+ sec: 'Seconds',
+ };
+
+ function lastNext(type, range, modifier) {
+ let diff;
+ const day = days[range];
+
+ if (typeof day !== 'undefined') {
+ diff = day - date.getDay();
+
+ if (diff === 0) {
+ diff = 7 * modifier;
+ } else if (diff > 0 && type === 'last') {
+ diff -= 7;
+ } else if (diff < 0 && type === 'next') {
+ diff += 7;
+ }
+
+ date.setDate(date.getDate() + diff);
+ }
+ }
+
+ function process(val) {
+ var splt = val.split(' '), // Todo: Reconcile this with regex using \s, taking into account browser issues with split and regexes
+ type = splt[0],
+ range = splt[1].substring(0, 3),
+ typeIsNumber = /\d+/.test(type),
+ ago = splt[2] === 'ago',
+ num = (type === 'last' ? -1 : 1) * (ago ? -1 : 1);
+
+ if (typeIsNumber) {
+ num *= parseInt(type, 10);
+ }
+
+ if (ranges.hasOwnProperty(range) && !splt[1].match(/^mon(day|\.)?$/i)) {
+ return date['set' + ranges[range]](
+ date['get' + ranges[range]]() + num,
+ );
+ }
+
+ if (range === 'wee') {
+ return date.setDate(date.getDate() + num * 7);
+ }
+
+ if (type === 'next' || type === 'last') {
+ lastNext(type, range, num);
+ } else if (!typeIsNumber) {
+ return false;
+ }
+
+ return true;
+ }
+
+ times =
+ '(years?|months?|weeks?|days?|hours?|minutes?|min|seconds?|sec' +
+ '|sunday|sun\\.?|monday|mon\\.?|tuesday|tue\\.?|wednesday|wed\\.?' +
+ '|thursday|thu\\.?|friday|fri\\.?|saturday|sat\\.?)';
+ regex =
+ '([+-]?\\d+\\s' + times + '|' + '(last|next)\\s' + times + ')(\\sago)?';
+
+ match = text.match(new RegExp(regex, 'gi'));
+ if (!match) {
+ return fail;
+ }
+
+ for (i = 0, len = match.length; i < len; i++) {
+ if (!process(match[i])) {
+ return fail;
+ }
+ }
+
+ // ECMAScript 5 only
+ // if (!match.every(process))
+ // return false;
+
+ return date.getTime() / 1000;
+}
diff --git a/packages/melody-runtime/src/functions.js b/packages/melody-runtime/src/functions.js
new file mode 100644
index 0000000..8d87a69
--- /dev/null
+++ b/packages/melody-runtime/src/functions.js
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ isNumber,
+ random as rand,
+ min as lMin,
+ max as lMax,
+ values,
+ upperFirst,
+ isFunction,
+ isObject,
+} from 'lodash';
+
+const MAX_SAFE_INTEGER =
+ 'MAX_SAFE_INTEGER' in Number ? Number.MAX_SAFE_INTEGER : 9007199254740991;
+
+export function random(
+ iterable: Array | Number | String = MAX_SAFE_INTEGER,
+): any {
+ if (isNumber(iterable)) {
+ return rand(0, iterable);
+ }
+ return iterable[rand(0, iterable.length - 1)];
+}
+
+export function min(iterable, ...additional) {
+ if (additional.length) {
+ return lMin([iterable, ...additional]);
+ }
+ if (Array.isArray(iterable)) {
+ return lMin(iterable);
+ }
+ return lMin(values(iterable));
+}
+
+export function max(iterable, ...additional) {
+ if (additional.length) {
+ return lMax([iterable, ...additional]);
+ }
+ if (Array.isArray(iterable)) {
+ return lMax(iterable);
+ }
+ return lMax(values(iterable));
+}
+
+export function cycle(data, index) {
+ return data[index % data.length];
+}
+
+export function attribute(obj, prop, args) {
+ if (Array.isArray(obj)) {
+ return obj[prop];
+ }
+ if (isObject(obj)) {
+ if (obj.hasOwnProperty(prop)) {
+ return isFunction(obj[prop]) ? obj[prop](...args) : obj[prop];
+ }
+ const ucProp = upperFirst(prop);
+ if (isFunction(obj['get' + ucProp])) {
+ return obj['get' + ucProp]();
+ }
+ if (isFunction(obj['is' + ucProp])) {
+ return obj['is' + ucProp]();
+ }
+ }
+ return undefined;
+}
diff --git a/packages/melody-runtime/src/helpers.js b/packages/melody-runtime/src/helpers.js
new file mode 100644
index 0000000..5183e34
--- /dev/null
+++ b/packages/melody-runtime/src/helpers.js
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export function inheritBlocks(template, usedTemplate, mappings) {
+ for (const name in usedTemplate) {
+ if (!usedTemplate.hasOwnProperty(name)) {
+ continue;
+ }
+
+ if (name !== 'render') {
+ if (mappings) {
+ const mappedName =
+ mappings[
+ name.substring(0, 7) +
+ name[7].toLowerCase() +
+ name.substring(8)
+ ];
+ if (mappedName) {
+ template[mappedName] = usedTemplate[name];
+ }
+ } else {
+ template[name] = usedTemplate[name];
+ }
+ }
+ }
+}
+
+export function isEmpty(val) {
+ return val !== 0 && (!val || (Array.isArray(val) && !val.length));
+}
diff --git a/packages/melody-runtime/src/index.js b/packages/melody-runtime/src/index.js
new file mode 100644
index 0000000..3b3828f
--- /dev/null
+++ b/packages/melody-runtime/src/index.js
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { createSubContext } from './context';
+export {
+ batch,
+ attrs,
+ styles,
+ classes,
+ merge,
+ replace,
+ reverse,
+ round,
+ striptags,
+ number_format,
+ format,
+ title,
+ url_encode,
+ strtotime,
+} from './filters';
+export { random, min, max, cycle, attribute } from './functions';
+export { isEmpty, inheritBlocks } from './helpers';
diff --git a/packages/melody-traverse/package.json b/packages/melody-traverse/package.json
new file mode 100644
index 0000000..65a535c
--- /dev/null
+++ b/packages/melody-traverse/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "melody-traverse",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "melody-types": "^0.10.0"
+ },
+ "devDependencies": {
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-traverse/src/Binding.js b/packages/melody-traverse/src/Binding.js
new file mode 100644
index 0000000..48a09a7
--- /dev/null
+++ b/packages/melody-traverse/src/Binding.js
@@ -0,0 +1,61 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export class Binding {
+ constructor(identifier, scope, path, kind = 'global') {
+ this.identifier = identifier;
+ this.scope = scope;
+ this.path = path;
+ this.kind = kind;
+
+ this.referenced = false;
+ this.references = 0;
+ this.referencePaths = [];
+ this.definitionPaths = [];
+ this.shadowedBinding = null;
+ this.contextual = false;
+
+ this.data = Object.create(null);
+ }
+
+ getData(key) {
+ return this.data[key];
+ }
+
+ setData(key, value) {
+ this.data[key] = value;
+ }
+
+ reference(path) {
+ this.referenced = true;
+ this.references++;
+ this.referencePaths.push(path);
+ }
+
+ // dereference(path) {
+ // if (path) {
+ // this.referencePaths.splice(this.referencePaths.indexOf(path), 1);
+ // }
+ // this.references--;
+ // this.referenced = !!this.references;
+ // }
+
+ getRootDefinition() {
+ if (this.shadowedBinding) {
+ return this.shadowedBinding.getRootDefinition();
+ }
+ return this;
+ }
+}
diff --git a/packages/melody-traverse/src/Path.js b/packages/melody-traverse/src/Path.js
new file mode 100644
index 0000000..81c6231
--- /dev/null
+++ b/packages/melody-traverse/src/Path.js
@@ -0,0 +1,403 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { PATH_CACHE_KEY } from 'melody-types';
+import { Node, is } from 'melody-types';
+import { visit } from './traverse';
+import Scope from './Scope';
+
+export default class Path {
+ //region Path creation
+ constructor(parent) {
+ this.parent = parent;
+
+ this.inList = false;
+ this.listKey = null;
+ this.parentKey = null;
+ this.container = null;
+ this.parentPath = null;
+
+ this.key = null;
+ this.node = null;
+ this.type = null;
+
+ this.state = null;
+
+ this.data = Object.create(null);
+ this.contexts = [];
+ this.scope = null;
+ this.visitor = null;
+
+ this.shouldSkip = false;
+ this.shouldStop = false;
+ this.removed = false;
+ }
+
+ static get({ parentPath, parent, container, listKey, key }): Path {
+ const targetNode = container[key],
+ paths =
+ (parent && parent[PATH_CACHE_KEY]) ||
+ (parent ? (parent[PATH_CACHE_KEY] = []) : []);
+ let path;
+
+ for (let i = 0, len = paths.length; i < len; i++) {
+ const candidate = paths[i];
+ if (candidate.node === targetNode) {
+ path = candidate;
+ break;
+ }
+ }
+
+ if (!path) {
+ path = new Path(parent);
+ }
+
+ path.inList = !!listKey;
+ path.listKey = listKey;
+ path.parentKey = listKey || key;
+ path.container = container;
+ path.parentPath = parentPath || path.parentPath;
+
+ path.key = key;
+ path.node = path.container[path.key];
+ path.type = path.node && path.node.type;
+
+ if (!path.node) {
+ /*eslint no-console: off*/
+ console.log(
+ 'Path has no node ' + path.parentKey + ' > ' + path.key,
+ );
+ }
+ paths.push(path);
+
+ return path;
+ }
+
+ //endregion
+
+ //region Generic data
+ setData(key: string, val: any): any {
+ return (this.data[key] = val);
+ }
+
+ getData(key: string, def?: any): any {
+ const val = this.data[key];
+ if (!val && def) {
+ return (this.data[key] = def);
+ }
+ return val;
+ }
+ //endregion
+
+ //region Context
+ pushContext(context) {
+ this.contexts.push(context);
+ this.setContext(context);
+ }
+
+ popContext() {
+ this.contexts.pop();
+ this.setContext(this.contexts[this.contexts.length - 1]);
+ }
+
+ setContext(context) {
+ this.shouldSkip = false;
+ this.shouldStop = false;
+ this.removed = false;
+ //this.skipKeys = {};
+
+ if (context) {
+ this.context = context;
+ this.state = context.state;
+ this.visitor = context.visitor;
+ }
+
+ this.setScope();
+ return this;
+ }
+
+ getScope(scope: Scope) {
+ if (Node.isScope(this.node)) {
+ if (this.node.type === 'BlockStatement') {
+ return Scope.get(this, scope.getRootScope());
+ }
+ return Scope.get(this, scope);
+ }
+ return scope;
+ }
+
+ setScope() {
+ let target = this.context && this.context.scope;
+
+ if (!target) {
+ let path = this.parentPath;
+ while (path && !target) {
+ target = path.scope;
+ path = path.parentPath;
+ }
+ }
+
+ this.scope = this.getScope(target);
+ }
+
+ visit(): boolean {
+ if (!this.node) {
+ return false;
+ }
+
+ if (call(this, 'enter') || this.shouldSkip) {
+ return this.shouldStop;
+ }
+
+ visit(this.node, this.visitor, this.scope, this.state, this);
+
+ call(this, 'exit');
+
+ return this.shouldStop;
+ }
+
+ skip() {
+ this.shouldSkip = true;
+ }
+
+ stop() {
+ this.shouldStop = true;
+ this.shouldSkip = true;
+ }
+
+ resync() {
+ if (this.removed) {
+ return;
+ }
+
+ if (this.parentPath) {
+ this.parent = this.parentPath.node;
+ }
+
+ if (this.parent && this.inList) {
+ const newContainer = this.parent[this.listKey];
+ if (this.container !== newContainer) {
+ this.container = newContainer || null;
+ }
+ }
+
+ if (this.container && this.node !== this.container[this.key]) {
+ this.key = null;
+ if (Array.isArray(this.container)) {
+ let i, len;
+ for (i = 0, len = this.container.length; i < len; i++) {
+ if (this.container[i] === this.node) {
+ this.setKey(i);
+ break;
+ }
+ }
+ } else {
+ let key;
+ for (key in this.container) {
+ if (this.container[key] === this.node) {
+ this.setKey(key);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ setKey(key) {
+ this.key = key;
+ this.node = this.container[this.key];
+ this.type = this.node && this.node.type;
+ }
+
+ requeue(path = this) {
+ if (path.removed) {
+ return;
+ }
+
+ for (const context of this.contexts) {
+ context.maybeQueue(path);
+ }
+ }
+ //endregion
+
+ //region Modification
+ replaceWith(value) {
+ this.resync();
+
+ const replacement = value instanceof Path ? value.node : value;
+
+ if (this.node === replacement) {
+ return;
+ }
+
+ replaceWith(this, replacement);
+ this.type = replacement.type;
+ this.resync();
+ this.setScope();
+ this.requeue();
+ }
+
+ replaceWithJS(replacement) {
+ this.resync();
+ replaceWith(this, replacement);
+ this.type = replacement.type;
+ this.resync();
+ this.setScope();
+ }
+
+ replaceWithMultipleJS(...replacements) {
+ this.resync();
+
+ if (!this.container) {
+ throw new Error('Path does not have a container');
+ }
+ if (!Array.isArray(this.container)) {
+ throw new Error('Container of path is not an array');
+ }
+
+ this.container.splice(this.key, 1, ...replacements);
+ this.resync();
+ this.updateSiblingKeys(this.key, replacements.length - 1);
+ markRemoved(this);
+ //this.node = replacements[0];
+ }
+
+ remove() {
+ this.resync();
+
+ if (Array.isArray(this.container)) {
+ this.container.splice(this.key, 1);
+ this.updateSiblingKeys(this.key, -1);
+ } else {
+ replaceWith(this, null);
+ }
+
+ markRemoved(this);
+ }
+
+ updateSiblingKeys(fromIndex, incrementBy) {
+ if (!this.parent) {
+ return;
+ }
+
+ const paths: Array = this.parent[PATH_CACHE_KEY];
+ for (const path of paths) {
+ if (path.key >= fromIndex) {
+ path.key += incrementBy;
+ }
+ }
+ }
+ //endregion
+
+ is(type: String) {
+ return is(this.node, type);
+ }
+
+ findParentPathOfType(type: String) {
+ let path = this.parentPath;
+ while (path && !path.is(type)) {
+ path = path.parentPath;
+ }
+ return path && path.type === type ? path : null;
+ }
+
+ get(key) {
+ let parts: Array = key.split('.'),
+ context = this.context;
+ if (parts.length === 1) {
+ let node = this.node,
+ container = node[key];
+ if (Array.isArray(container)) {
+ return container.map((_, i) =>
+ Path.get({
+ listKey: key,
+ parentPath: this,
+ parent: node,
+ container,
+ key: i,
+ }).setContext(context),
+ );
+ } else {
+ return Path.get({
+ parentPath: this,
+ parent: node,
+ container: node,
+ key,
+ }).setContext(context);
+ }
+ } else {
+ let path = this;
+ for (const part of parts) {
+ if (Array.isArray(path)) {
+ path = path[part];
+ } else {
+ path = path.get(part);
+ }
+ }
+ return path;
+ }
+ }
+}
+
+function markRemoved(path) {
+ path.shouldSkip = true;
+ path.removed = true;
+ path.node = null;
+}
+
+function replaceWith(path, node) {
+ if (!path.container) {
+ throw new Error('Path does not have a container');
+ }
+
+ path.node = path.container[path.key] = node;
+}
+
+function call(path, key): boolean {
+ if (!path.node) {
+ return false;
+ }
+
+ const visitor = path.visitor[path.node.type];
+ if (!visitor || !visitor[key]) {
+ return false;
+ }
+
+ const fns: Array = visitor[key];
+ for (let i = 0, len = fns.length; i < len; i++) {
+ const fn = fns[i];
+ if (!fn) {
+ continue;
+ }
+
+ const node = path.node;
+ if (!node) {
+ return true;
+ }
+
+ fn.call(path.state, path, path.state);
+
+ // node has been replaced, requeue
+ if (path.node !== node) {
+ return true;
+ }
+
+ if (path.shouldStop || path.shouldSkip || path.removed) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/packages/melody-traverse/src/Scope.js b/packages/melody-traverse/src/Scope.js
new file mode 100644
index 0000000..62badf3
--- /dev/null
+++ b/packages/melody-traverse/src/Scope.js
@@ -0,0 +1,183 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { Binding } from './Binding';
+import type Path from './Path';
+const CACHE_KEY = Symbol();
+let uid = 0;
+
+export default class Scope {
+ constructor(path: Path, parentScope?: Scope) {
+ this.uid = uid++;
+ this.parent = parentScope;
+
+ this.parentBlock = path.parent;
+ this.block = path.node;
+ this.path = path;
+
+ this.references = Object.create(null);
+ this.bindings = Object.create(null);
+ this.globals = Object.create(null);
+ this.uids = Object.create(null);
+ this.escapesContext = false;
+ this._contextName = null;
+ this.mutated = false;
+ //this.contextName = parentScope && parentScope.contextName || '_context';
+ }
+
+ set contextName(val) {
+ this._contextName = val;
+ }
+
+ get contextName() {
+ if (this._contextName) {
+ return this._contextName;
+ }
+ if (this.parent) {
+ return this.parent.contextName || '_context';
+ }
+ return '_context';
+ }
+
+ static get(path: Path, parentScope?: Scope) {
+ if (parentScope && parentScope.block == path.node) {
+ return parentScope;
+ }
+
+ const cached = getCache(path.node);
+ if (cached) {
+ return cached;
+ }
+
+ const scope = new Scope(path, parentScope);
+ path.node[CACHE_KEY] = scope;
+ return scope;
+ }
+
+ get needsSubContext() {
+ return this.escapesContext && this.hasCustomBindings;
+ }
+
+ get hasCustomBindings() {
+ return !!Object.keys(this.bindings).length;
+ }
+
+ getBinding(name: string) {
+ let scope = this;
+
+ do {
+ const binding = scope.getOwnBinding(name);
+ if (binding) {
+ return binding;
+ }
+ if (scope.path.is('RootScope')) {
+ return;
+ }
+ } while ((scope = scope.parent));
+ }
+
+ getOwnBinding(name: string) {
+ return this.bindings[name];
+ }
+
+ hasOwnBinding(name: string) {
+ return !!this.getOwnBinding(name);
+ }
+
+ hasBinding(name: string) {
+ return !name
+ ? false
+ : !!(this.hasOwnBinding(name) || this.parentHasBinding(name));
+ }
+
+ getRootScope() {
+ let scope = this;
+ while (scope.parent) {
+ scope = scope.parent;
+ }
+ return scope;
+ }
+
+ registerBinding(name: string, path: Path = null, kind: string = 'context') {
+ let scope = this;
+ if (kind === 'global' && path === null) {
+ scope = this.getRootScope();
+ } else if (kind === 'const') {
+ while (scope.parent) {
+ scope = scope.parent;
+ if (scope.path.is('RootScope')) {
+ break;
+ }
+ }
+ }
+ // todo identify if we need to be able to differentiate between binding kinds
+ // if (scope.bindings[name]) {
+ // todo: warn about colliding binding or fix it
+ // }
+ if (this.path.state) {
+ this.path.state.markIdentifier(name);
+ }
+ return (scope.bindings[name] = new Binding(name, this, path, kind));
+ }
+
+ reference(name: string, path: Path) {
+ let binding = this.getBinding(name);
+ if (!binding) {
+ binding = this.registerBinding(name);
+ }
+ binding.reference(path);
+ }
+
+ parentHasBinding(name: string) {
+ return this.parent && this.parent.hasBinding(name);
+ }
+
+ generateUid(nameHint: string = 'temp') {
+ const name = toIdentifier(nameHint);
+
+ let uid,
+ i = 0;
+ do {
+ uid = generateUid(name, i);
+ i++;
+ } while (this.hasBinding(uid));
+
+ return uid;
+ }
+}
+
+function getCache(node) {
+ return node[CACHE_KEY];
+}
+
+function toIdentifier(nameHint) {
+ let name = nameHint + '';
+ name = name.replace(/[^a-zA-Z0-9$_]/g, '');
+
+ name = name.replace(/^[-0-9]+/, '');
+ name = name.replace(/[-\s]+(.)?/, function(match, c) {
+ return c ? c.toUpperCase() : '';
+ });
+
+ name = name.replace(/^_+/, '').replace(/[0-9]+$/, '');
+ return name;
+}
+
+function generateUid(name, i) {
+ if (i > 0) {
+ return `_${name}$${i}`;
+ }
+ return `_${name}`;
+}
diff --git a/packages/melody-traverse/src/TraversalContext.js b/packages/melody-traverse/src/TraversalContext.js
new file mode 100644
index 0000000..35dc768
--- /dev/null
+++ b/packages/melody-traverse/src/TraversalContext.js
@@ -0,0 +1,151 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Path from './Path';
+
+export default class TraversalContext {
+ constructor(scope, visitor, state, parentPath) {
+ this.parentPath = parentPath;
+ this.scope = scope;
+ this.state = state;
+ this.visitor = visitor;
+
+ this.queue = null;
+ this.priorityQueue = null;
+ }
+
+ create(parent, container, key, listKey): Path {
+ return Path.get({
+ parentPath: this.parentPath,
+ parent,
+ container,
+ key,
+ listKey,
+ });
+ }
+
+ shouldVisit(node): boolean {
+ const visitor = this.visitor;
+
+ if (visitor[node.type]) {
+ return true;
+ }
+
+ const keys: Array = node.visitorKeys;
+ // this node doesn't have any children
+ if (!keys || !keys.length) {
+ return false;
+ }
+
+ let i, len;
+ for (i = 0, len = keys.length; i < len; i++) {
+ // check if some of its visitor keys have a value,
+ // if so, we need to visit it
+ if (node[keys[i]]) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ visit(node, key) {
+ var nodes = node[key];
+ if (!nodes) {
+ return false;
+ }
+
+ if (Array.isArray(nodes)) {
+ return this.visitMultiple(nodes, node, key);
+ } else {
+ return this.visitSingle(node, key);
+ }
+ }
+
+ visitSingle(node, key): boolean {
+ if (this.shouldVisit(node[key])) {
+ return this.visitQueue([this.create(node, node, key)]);
+ } else {
+ return false;
+ }
+ }
+
+ visitMultiple(container, parent, listKey) {
+ if (!container.length) {
+ return false;
+ }
+
+ const queue = [];
+
+ for (let i = 0, len = container.length; i < len; i++) {
+ const node = container[i];
+ if (node && this.shouldVisit(node)) {
+ queue.push(this.create(parent, container, i, listKey));
+ }
+ }
+
+ return this.visitQueue(queue);
+ }
+
+ visitQueue(queue: Array) {
+ this.queue = queue;
+ this.priorityQueue = [];
+
+ let visited = [],
+ stop = false;
+
+ for (const path of queue) {
+ path.resync();
+ path.pushContext(this);
+
+ if (visited.indexOf(path.node) >= 0) {
+ continue;
+ }
+ visited.push(path.node);
+
+ if (path.visit()) {
+ stop = true;
+ break;
+ }
+
+ if (this.priorityQueue.length) {
+ stop = this.visitQueue(this.priorityQueue);
+ this.priorityQueue = [];
+ this.queue = queue;
+ if (stop) {
+ break;
+ }
+ }
+ }
+
+ for (const path of queue) {
+ path.popContext();
+ }
+
+ this.queue = null;
+
+ return stop;
+ }
+
+ maybeQueue(path, notPriority?: boolean) {
+ if (this.queue) {
+ if (notPriority) {
+ this.queue.push(path);
+ } else {
+ this.priorityQueue.push(path);
+ }
+ }
+ }
+}
diff --git a/packages/melody-traverse/src/index.js b/packages/melody-traverse/src/index.js
new file mode 100644
index 0000000..24d2a14
--- /dev/null
+++ b/packages/melody-traverse/src/index.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Path from './Path';
+import Scope from './Scope';
+export { Scope, Path };
+export * from './Scope';
+export { merge, explode } from './visitors';
+export { traverse, visit } from './traverse';
diff --git a/packages/melody-traverse/src/traverse.js b/packages/melody-traverse/src/traverse.js
new file mode 100644
index 0000000..7d31c97
--- /dev/null
+++ b/packages/melody-traverse/src/traverse.js
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { explode } from './visitors';
+import TraversalContext from './TraversalContext';
+
+export function traverse(
+ parentNode,
+ visitor: Object,
+ scope?: Object,
+ state?: Object = {},
+ parentPath?: Object,
+) {
+ if (!parentNode) {
+ return;
+ }
+
+ explode(visitor);
+ visit(parentNode, visitor, scope, state, parentPath);
+}
+
+export function visit(node, visitor, scope, state, parentPath) {
+ const keys: Array = node.visitorKeys;
+ if (!keys || !keys.length) {
+ return;
+ }
+
+ const context = new TraversalContext(scope, visitor, state, parentPath);
+ for (let i = 0, len = keys.length; i < len; i++) {
+ const key = keys[i];
+ if (context.visit(node, key)) {
+ return;
+ }
+ }
+}
diff --git a/packages/melody-traverse/src/visitors.js b/packages/melody-traverse/src/visitors.js
new file mode 100644
index 0000000..0f40f70
--- /dev/null
+++ b/packages/melody-traverse/src/visitors.js
@@ -0,0 +1,109 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { ALIAS_TO_TYPE } from 'melody-types';
+
+const EXPLODED = Symbol();
+
+export function explode(visitor) {
+ if (visitor[EXPLODED]) {
+ return visitor;
+ }
+ visitor[EXPLODED] = true;
+
+ for (const key of Object.getOwnPropertyNames(visitor)) {
+ // make sure all members are objects with enter and exit methods
+ let fns = visitor[key];
+ if (typeof fns === 'function') {
+ fns = visitor[key] = { enter: fns };
+ }
+
+ // make sure enter and exit are arrays
+ if (fns.enter && !Array.isArray(fns.enter)) {
+ fns.enter = [fns.enter];
+ }
+ if (fns.exit && !Array.isArray(fns.exit)) {
+ fns.exit = [fns.exit];
+ }
+ }
+
+ let j = 0;
+ const visitorKeys = Object.getOwnPropertyNames(visitor);
+ const visitorKeyLength = visitorKeys.length;
+ for (; j < visitorKeyLength; j++) {
+ const key = visitorKeys[j];
+ // manage aliases
+ if (ALIAS_TO_TYPE[key]) {
+ let i = 0;
+ for (
+ const types = ALIAS_TO_TYPE[key], len = types.length;
+ i < len;
+ i++
+ ) {
+ const type = types[i];
+ if (!visitor[type]) {
+ visitor[type] = { enter: [] };
+ }
+ if (visitor[key].enter) {
+ visitor[type].enter.push(...visitor[key].enter);
+ }
+ if (visitor[key].exit) {
+ if (!visitor[type].exit) {
+ visitor[type].exit = [];
+ }
+ visitor[type].exit.push(...visitor[key].exit);
+ }
+ }
+ delete visitor[key];
+ }
+ }
+}
+
+export function merge(...visitors: Array) {
+ const rootVisitor = {};
+
+ let i = 0;
+ for (const len = visitors.length; i < len; i++) {
+ const visitor = visitors[i];
+ explode(visitor);
+
+ let j = 0;
+ const visitorTypes = Object.getOwnPropertyNames(visitor);
+ for (
+ const numberOfTypes = visitorTypes.length;
+ j < numberOfTypes;
+ j++
+ ) {
+ const key = visitorTypes[j];
+ const visitorType = visitor[key];
+
+ if (!rootVisitor[key]) {
+ rootVisitor[key] = {};
+ }
+
+ const nodeVisitor = rootVisitor[key];
+ nodeVisitor.enter = [].concat(
+ nodeVisitor.enter || [],
+ visitorType.enter || [],
+ );
+ nodeVisitor.exit = [].concat(
+ nodeVisitor.exit || [],
+ visitorType.exit || [],
+ );
+ }
+ }
+
+ return rootVisitor;
+}
diff --git a/packages/melody-types/package.json b/packages/melody-types/package.json
new file mode 100644
index 0000000..e56405d
--- /dev/null
+++ b/packages/melody-types/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "melody-types",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "babel-types": "^6.8.1"
+ },
+ "bundledDependencies": [
+ "babel-types"
+ ],
+ "devDependencies": {
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-types/src/index.js b/packages/melody-types/src/index.js
new file mode 100644
index 0000000..9a72c55
--- /dev/null
+++ b/packages/melody-types/src/index.js
@@ -0,0 +1,358 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as t from 'babel-types';
+
+export const TYPE_MAP = Object.create(null);
+export const ALIAS_TO_TYPE = Object.create(null);
+export const PATH_CACHE_KEY = Symbol();
+
+const IS_ALIAS_OF = Object.create(null);
+
+export class Node {
+ constructor() {
+ this.loc = {
+ source: null,
+ start: { line: 0, column: 0 },
+ end: { line: 0, column: 0 },
+ };
+ this[PATH_CACHE_KEY] = [];
+ }
+
+ toJSON() {
+ return Object.getOwnPropertyNames(this).reduce(
+ (acc, name) => {
+ if (name === 'loc' || name === 'parent') {
+ return acc;
+ }
+ const value = this[name];
+ if (Array.isArray(value)) {
+ acc[name] = value.map(val => val.toJSON());
+ } else {
+ acc[name] = value && value.toJSON ? value.toJSON() : value;
+ }
+ return acc;
+ },
+ {
+ type: this.type,
+ },
+ );
+ }
+
+ static registerType(type) {
+ if (Node['is' + type]) {
+ return;
+ }
+
+ Node['is' + type] = function(node) {
+ return is(node, type);
+ };
+
+ // Node['assert' + type] = function(node) {
+ // if (!is(node, type)) {
+ // throw new Error('Expected node to be of type ' + type + ' but was ' + (node ? node.type : 'undefined') + ' instead');
+ // }
+ // };
+ }
+}
+Node.registerType('Scope');
+
+export function is(node, type) {
+ return (
+ node &&
+ (node.type === type ||
+ (IS_ALIAS_OF[type] && IS_ALIAS_OF[type][node.type]) ||
+ t.is(node, type))
+ );
+}
+
+export function visitor(type, ...fields: String) {
+ type.prototype.visitorKeys = fields;
+}
+
+export function alias(type, ...aliases: String) {
+ type.prototype.aliases = aliases;
+ for (let i = 0, len = aliases.length; i < len; i++) {
+ const alias = aliases[i];
+ if (!ALIAS_TO_TYPE[alias]) {
+ ALIAS_TO_TYPE[alias] = [];
+ }
+ ALIAS_TO_TYPE[alias].push(type.prototype.type);
+ if (!IS_ALIAS_OF[alias]) {
+ IS_ALIAS_OF[alias] = {};
+ }
+ IS_ALIAS_OF[alias][type.prototype.type] = true;
+ Node.registerType(alias);
+ }
+}
+
+export function type(Type, type: String) {
+ Type.prototype.type = type;
+ TYPE_MAP[type] = Type;
+
+ Node.registerType(type);
+}
+
+export class Fragment extends Node {
+ constructor(expression: Node) {
+ super();
+ this.value = expression;
+ }
+}
+type(Fragment, 'Fragment');
+alias(Fragment, 'Statement');
+visitor(Fragment, 'value');
+
+export class PrintExpressionStatement extends Node {
+ constructor(expression: Node) {
+ super();
+ this.value = expression;
+ }
+}
+type(PrintExpressionStatement, 'PrintExpressionStatement');
+alias(PrintExpressionStatement, 'Statement', 'PrintStatement');
+visitor(PrintExpressionStatement, 'value');
+
+export class PrintTextStatement extends Node {
+ constructor(text: StringLiteral) {
+ super();
+ this.value = text;
+ }
+}
+type(PrintTextStatement, 'PrintTextStatement');
+alias(PrintTextStatement, 'Statement', 'PrintStatement');
+visitor(PrintTextStatement, 'value');
+
+export class ConstantValue extends Node {
+ constructor(value) {
+ super();
+ this.value = value;
+ }
+
+ toString() {
+ return `Const(${this.value})`;
+ }
+}
+alias(ConstantValue, 'Expression', 'Literal', 'Immutable');
+
+export class StringLiteral extends ConstantValue {}
+type(StringLiteral, 'StringLiteral');
+alias(StringLiteral, 'Expression', 'Literal', 'Immutable');
+
+export class NumericLiteral extends ConstantValue {}
+type(NumericLiteral, 'NumericLiteral');
+alias(NumericLiteral, 'Expression', 'Literal', 'Immutable');
+
+export class BooleanLiteral extends ConstantValue {
+ constructor(value) {
+ super(value);
+ }
+}
+type(BooleanLiteral, 'BooleanLiteral');
+alias(BooleanLiteral, 'Expression', 'Literal', 'Immutable');
+
+export class NullLiteral extends ConstantValue {
+ constructor() {
+ super(null);
+ }
+}
+type(NullLiteral, 'NullLiteral');
+alias(NullLiteral, 'Expression', 'Literal', 'Immutable');
+
+export class Identifier extends Node {
+ constructor(name) {
+ super();
+ this.name = name;
+ }
+}
+type(Identifier, 'Identifier');
+alias(Identifier, 'Expression');
+
+export class UnaryExpression extends Node {
+ constructor(operator: String, argument: Node) {
+ super();
+ this.operator = operator;
+ this.argument = argument;
+ }
+}
+type(UnaryExpression, 'UnaryExpression');
+alias(UnaryExpression, 'Expression', 'UnaryLike');
+visitor(UnaryExpression, 'argument');
+
+export class BinaryExpression extends Node {
+ constructor(operator: String, left: Node, right: Node) {
+ super();
+ this.operator = operator;
+ this.left = left;
+ this.right = right;
+ }
+}
+type(BinaryExpression, 'BinaryExpression');
+alias(BinaryExpression, 'Binary', 'Expression');
+visitor(BinaryExpression, 'left', 'right');
+
+export class BinaryConcatExpression extends BinaryExpression {
+ constructor(left: Node, right: Node) {
+ super('~', left, right);
+ }
+}
+type(BinaryConcatExpression, 'BinaryConcatExpression');
+alias(BinaryConcatExpression, 'BinaryExpression', 'Binary', 'Expression');
+visitor(BinaryConcatExpression, 'left', 'right');
+
+export class ConditionalExpression extends Node {
+ constructor(test: Node, consequent: Node, alternate: Node) {
+ super();
+ this.test = test;
+ this.consequent = consequent;
+ this.alternate = alternate;
+ }
+}
+type(ConditionalExpression, 'ConditionalExpression');
+alias(ConditionalExpression, 'Expression', 'Conditional');
+visitor(ConditionalExpression, 'test', 'consequent', 'alternate');
+
+export class ArrayExpression extends Node {
+ constructor(elements = []) {
+ super();
+ this.elements = elements;
+ }
+}
+type(ArrayExpression, 'ArrayExpression');
+alias(ArrayExpression, 'Expression');
+visitor(ArrayExpression, 'elements');
+
+export class MemberExpression extends Node {
+ constructor(object: Node, property: Node, computed: boolean) {
+ super();
+ this.object = object;
+ this.property = property;
+ this.computed = computed;
+ }
+}
+type(MemberExpression, 'MemberExpression');
+alias(MemberExpression, 'Expression', 'LVal');
+visitor(MemberExpression, 'object', 'property');
+
+export class CallExpression extends Node {
+ constructor(callee: Node, args: Array) {
+ super();
+ this.callee = callee;
+ this.arguments = args;
+ }
+}
+type(CallExpression, 'CallExpression');
+alias(CallExpression, 'Expression', 'FunctionInvocation');
+visitor(CallExpression, 'callee', 'arguments');
+
+export class NamedArgumentExpression extends Node {
+ constructor(name: Identifier, value: Node) {
+ super();
+ this.name = name;
+ this.value = value;
+ }
+}
+type(NamedArgumentExpression, 'NamedArgumentExpression');
+alias(NamedArgumentExpression, 'Expression');
+visitor(NamedArgumentExpression, 'name', 'value');
+
+export class ObjectExpression extends Node {
+ constructor(properties: Array = []) {
+ super();
+ this.properties = properties;
+ }
+}
+type(ObjectExpression, 'ObjectExpression');
+alias(ObjectExpression, 'Expression');
+visitor(ObjectExpression, 'properties');
+
+export class ObjectProperty extends Node {
+ constructor(key: Node, value: Node, computed: boolean) {
+ super();
+ this.key = key;
+ this.value = value;
+ this.computed = computed;
+ }
+}
+type(ObjectProperty, 'ObjectProperty');
+alias(ObjectProperty, 'Property', 'ObjectMember');
+visitor(ObjectProperty, 'key', 'value');
+
+export class SequenceExpression extends Node {
+ constructor(expressions: Array = []) {
+ super();
+ this.expressions = expressions;
+ }
+
+ add(child: Node) {
+ this.expressions.push(child);
+ this.loc.end = child.loc.end;
+ }
+}
+type(SequenceExpression, 'SequenceExpression');
+alias(SequenceExpression, 'Expression', 'Scope');
+visitor(SequenceExpression, 'expressions');
+
+export class SliceExpression extends Node {
+ constructor(target: Node, start: Node, end: Node) {
+ super();
+ this.target = target;
+ this.start = start;
+ this.end = end;
+ }
+}
+type(SliceExpression, 'SliceExpression');
+alias(SliceExpression, 'Expression');
+visitor(SliceExpression, 'source', 'start', 'end');
+
+export class FilterExpression extends Node {
+ constructor(target: Node, name: Identifier, args: Array) {
+ super();
+ this.target = target;
+ this.name = name;
+ this.arguments = args;
+ }
+}
+type(FilterExpression, 'FilterExpression');
+alias(FilterExpression, 'Expression');
+visitor(FilterExpression, 'target', 'arguments');
+
+export class Element extends Node {
+ constructor(name: String) {
+ super();
+ this.name = name;
+ this.attributes = [];
+ this.children = [];
+ this.selfClosing = false;
+ }
+}
+type(Element, 'Element');
+alias(Element, 'Expression');
+visitor(Element, 'attributes', 'children');
+
+export class Attribute extends Node {
+ constructor(name: Node, value: Node = null) {
+ super();
+ this.name = name;
+ this.value = value;
+ }
+
+ isImmutable() {
+ return is(this.name, 'Identifier') && is(this.value, 'Immutable');
+ }
+}
+type(Attribute, 'Attribute');
+visitor(Attribute, 'name', 'value');
diff --git a/packages/melody-util/__tests__/MiddlewareSpec.js b/packages/melody-util/__tests__/MiddlewareSpec.js
new file mode 100644
index 0000000..d57148c
--- /dev/null
+++ b/packages/melody-util/__tests__/MiddlewareSpec.js
@@ -0,0 +1,328 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { assert } from 'chai';
+
+import { createComponent } from 'melody-component';
+import { applyMiddleware } from '../src';
+import thunkMiddleware from 'redux-thunk';
+import promiseMiddleware from 'redux-promise';
+
+describe('Middleware', function() {
+ const tpl = {
+ render(state) {
+ // do nothing
+ },
+ };
+
+ const countingReducer = (state = { count: 0 }, action) => {
+ switch (action.type) {
+ case 'inc':
+ return { ...state, count: state.count + 1 };
+ case 'dec':
+ return { ...state, count: state.count - 1 };
+ }
+ return state;
+ };
+
+ it('should be possible to create a component', function() {
+ const reducer = (state, action) => {
+ return action.payload;
+ };
+
+ const el = document.createElement('div');
+ const Component = createComponent(tpl, reducer);
+ const comp = new Component(el);
+ assert(!comp.getState(), 'Component does not have state');
+ comp.dispatch({ type: 'count', payload: { msg: 'hello' } });
+ assert(!!comp.getState(), 'Component has state');
+ assert.equal(comp.getState().msg, 'hello');
+ });
+
+ it('can call dispatch as often as it wants to', function() {
+ let reducerCalled = 0;
+ const reducer = (state = { count: 0 }, action) => {
+ if (action.type === 'count') {
+ reducerCalled++;
+ return { ...state, count: state.count + 1 };
+ }
+ return state;
+ };
+
+ const doubleDispatch = comp => next => action => {
+ next(action);
+ if (action.type === 'count') {
+ next(action);
+ }
+ };
+
+ const el = document.createElement('div');
+ const Component = createComponent(
+ tpl,
+ reducer,
+ applyMiddleware(doubleDispatch),
+ );
+ const comp = new Component(el);
+ comp.dispatch({ type: 'count' });
+ assert.equal(reducerCalled, 2, 'Reducer was called twice');
+ assert.equal(2, comp.getState().count, 'Action is reduced twice');
+ });
+
+ it('can filter dispatch calls', function() {
+ const filterType = type => comp => next => action => {
+ if (action.type === type) {
+ next(action);
+ }
+ };
+
+ const el = document.createElement('div');
+ const Component = createComponent(
+ tpl,
+ countingReducer,
+ applyMiddleware(filterType('inc')),
+ );
+ const comp = new Component(el);
+ comp.dispatch({ type: 'inc' });
+ comp.dispatch({ type: 'inc' });
+ comp.dispatch({ type: 'dec' });
+ comp.dispatch({ type: 'inc' });
+ assert.equal(3, comp.getState().count);
+ });
+
+ describe('thunk middleware', function() {
+ it('accepts a function as an action', function() {
+ const el = document.createElement('div');
+ const Component = createComponent(
+ tpl,
+ countingReducer,
+ applyMiddleware(thunkMiddleware, null, undefined, false),
+ );
+ const comp = new Component(el);
+ comp.dispatch(dispatch => {
+ dispatch({ type: 'inc' });
+ dispatch({ type: 'dec' });
+ dispatch({ type: 'inc' });
+ });
+ assert.equal(1, comp.getState().count);
+ });
+
+ it('passes getState to the actor', function() {
+ const el = document.createElement('div');
+ const Component = createComponent(
+ tpl,
+ countingReducer,
+ applyMiddleware(thunkMiddleware),
+ );
+ const comp = new Component(el);
+ comp.dispatch((dispatch, getState) => {
+ dispatch({ type: 'inc' });
+ assert.equal(1, getState().count, 'After first increment');
+ dispatch({ type: 'dec' });
+ assert.equal(0, getState().count, 'After decrement');
+ dispatch({ type: 'inc' });
+ assert.equal(1, getState().count, 'After last increment');
+ });
+ assert.equal(1, comp.getState().count);
+ });
+
+ it('can be async', function(done) {
+ const el = document.createElement('div');
+ const Component = createComponent(
+ tpl,
+ countingReducer,
+ applyMiddleware(thunkMiddleware),
+ );
+ const comp = new Component(el);
+ comp
+ .dispatch(
+ (dispatch, getState) =>
+ new Promise((resolve, reject) => {
+ dispatch({ type: 'inc' });
+ assert.equal(
+ 1,
+ getState().count,
+ 'After first increment',
+ );
+ dispatch({ type: 'dec' });
+ assert.equal(
+ 0,
+ getState().count,
+ 'After decrement',
+ );
+ dispatch({ type: 'inc' });
+ assert.equal(
+ 1,
+ getState().count,
+ 'After last increment',
+ );
+ resolve();
+ }),
+ )
+ .then(() => {
+ assert.equal(1, comp.getState().count);
+ })
+ .then(done, done);
+ });
+
+ it('dispatches to the next middleware', () => {
+ const el = document.createElement('div');
+ const log = [];
+ const loggerMiddleware = store => next => action => {
+ log.push(action);
+ next(action);
+ };
+ const Component = createComponent(
+ tpl,
+ countingReducer,
+ applyMiddleware(thunkMiddleware, loggerMiddleware),
+ );
+ const comp = new Component(el);
+ comp.dispatch(dispatch => dispatch({ type: 'inc' }));
+ expect(JSON.stringify(log)).toEqual(
+ JSON.stringify([{ type: 'inc' }]),
+ );
+ assert.equal(1, log.length, 'should have logged once');
+ });
+ });
+
+ describe('promise middleware', function() {
+ it('accepts a promise', function(done) {
+ const el = document.createElement('div');
+ const Component = createComponent(
+ tpl,
+ countingReducer,
+ applyMiddleware(promiseMiddleware),
+ );
+ const comp = new Component(el);
+ comp
+ .dispatch(
+ new Promise(resolve => {
+ resolve({ type: 'inc' });
+ }),
+ )
+ .then(() => {
+ assert.equal(1, comp.getState().count);
+ })
+ .then(done, done);
+ assert.equal(0, comp.getState().count);
+ });
+
+ it('ignores rejected promises', function(done) {
+ const el = document.createElement('div');
+ const Component = createComponent(
+ tpl,
+ countingReducer,
+ applyMiddleware(promiseMiddleware),
+ );
+ const comp = new Component(el);
+ comp
+ .dispatch(
+ new Promise((resolve, reject) => {
+ reject({ reason: 'just testing' });
+ }),
+ )
+ .then(() => {
+ assert.equal(1, comp.getState().count);
+ })
+ .catch(err => {
+ assert.equal(err.reason, 'just testing');
+ })
+ .then(done, done);
+ assert.equal(0, comp.getState().count);
+ });
+ });
+
+ describe('when combined', function() {
+ it('accepts a promise', function(done) {
+ const el = document.createElement('div');
+ const Component = createComponent(
+ tpl,
+ countingReducer,
+ applyMiddleware(promiseMiddleware, thunkMiddleware),
+ );
+ const comp = new Component(el);
+ comp
+ .dispatch(
+ new Promise(resolve => {
+ resolve({ type: 'inc' });
+ }),
+ )
+ .then(() => {
+ assert.equal(1, comp.getState().count);
+ })
+ .then(done, done);
+ assert.equal(0, comp.getState().count);
+ });
+
+ it('ignores rejected promises', function(done) {
+ const el = document.createElement('div');
+ const Component = createComponent(
+ tpl,
+ countingReducer,
+ applyMiddleware(promiseMiddleware, thunkMiddleware),
+ );
+ const comp = new Component(el);
+ comp
+ .dispatch(
+ new Promise((resolve, reject) => {
+ reject({ reason: 'just testing' });
+ }),
+ )
+ .then(() => {
+ assert.equal(1, comp.getState().count);
+ })
+ .catch(err => {
+ assert.equal(err.reason, 'just testing');
+ })
+ .then(done, done);
+ assert.equal(0, comp.getState().count);
+ });
+
+ it('accepts a function', function() {
+ const el = document.createElement('div');
+ const Component = createComponent(
+ tpl,
+ countingReducer,
+ applyMiddleware(promiseMiddleware, thunkMiddleware),
+ );
+ const comp = new Component(el);
+ comp.dispatch(dispatch => {
+ dispatch({ type: 'inc' });
+ dispatch({ type: 'dec' });
+ dispatch({ type: 'inc' });
+ });
+ assert.equal(1, comp.getState().count);
+ });
+
+ it('provides access to state', function() {
+ const el = document.createElement('div');
+ const Component = createComponent(
+ tpl,
+ countingReducer,
+ applyMiddleware(promiseMiddleware, thunkMiddleware),
+ );
+ const comp = new Component(el);
+ comp.dispatch((dispatch, getState) => {
+ dispatch({ type: 'inc' });
+ assert.equal(1, getState().count, 'After first increment');
+ dispatch({ type: 'dec' });
+ assert.equal(0, getState().count, 'After decrement');
+ dispatch({ type: 'inc' });
+ assert.equal(1, getState().count, 'After last increment');
+ });
+ assert.equal(1, comp.getState().count);
+ });
+ });
+});
diff --git a/packages/melody-util/__tests__/ReducerSpec.js b/packages/melody-util/__tests__/ReducerSpec.js
new file mode 100644
index 0000000..13c8c0c
--- /dev/null
+++ b/packages/melody-util/__tests__/ReducerSpec.js
@@ -0,0 +1,366 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { assert } from 'chai';
+import { RECEIVE_PROPS } from 'melody-component';
+
+import { createActionReducer, dispatchToState, exposeToState } from '../src';
+
+import sinon from 'sinon';
+
+describe('Reducer utils', function() {
+ describe('createActionReducer', function() {
+ it('delegates to the correct action handler', function() {
+ const reducer = createActionReducer(
+ {
+ inc(state, action) {
+ return Object.assign({}, state, {
+ count: state.count + 1,
+ });
+ },
+ dec(state, action) {
+ return Object.assign({}, state, {
+ count: state.count - 1,
+ });
+ },
+ noop(state, action) {},
+ },
+ { count: 5 },
+ );
+ assert.equal(reducer({ count: 1 }, { type: 'inc' }).count, 2);
+ assert.equal(reducer({ count: 2 }, { type: 'inc' }).count, 3);
+ assert.equal(reducer({ count: 2 }, { type: 'dec' }).count, 1);
+ assert.equal(reducer({ count: 2 }, { type: 'noop' }).count, 2);
+ assert.equal(reducer({ count: 2 }, { type: 'unknown' }).count, 2);
+ assert.equal(reducer(undefined, { type: 'unknown' }).count, 5);
+ assert.equal(reducer(undefined, { type: 'inc' }).count, 6);
+ assert.equal(reducer(undefined, { type: 'noop' }).count, 5);
+ });
+ });
+
+ describe('dispatchToState', function() {
+ it('should add the dispatch methods to state', function() {
+ const reducer = dispatchToState(dispatch => ({
+ test() {
+ dispatch({ type: 'test' });
+ },
+ }));
+ const dispatch = sinon.spy();
+
+ const state = reducer(
+ {},
+ {
+ type: 'MELODY/RECEIVE_PROPS',
+ meta: { dispatch },
+ },
+ );
+ assert(typeof state.test === 'function', 'test is a function');
+ state.test();
+ assert(dispatch.calledOnce);
+ assert(dispatch.calledWith({ type: 'test' }));
+ });
+
+ it('should invoke the dispatch mapper only once', function() {
+ const test = sinon.spy();
+ const dispatchMapper = sinon.stub().returns({ test });
+ const reducer = dispatchToState(dispatchMapper);
+ const dispatch = sinon.spy();
+
+ let state = reducer(
+ {},
+ {
+ type: 'MELODY/RECEIVE_PROPS',
+ meta: { dispatch },
+ },
+ );
+ state = reducer(state, {
+ type: 'MELODY/RECEIVE_PROPS',
+ meta: { dispatch },
+ });
+ state = reducer(state, {
+ type: 'fun',
+ meta: { dispatch },
+ });
+ assert(
+ dispatchMapper.calledOnce,
+ 'dispatch mapper invoked exactly once',
+ );
+ assert(typeof state.test === 'function', 'test is a function');
+ });
+ });
+
+ describe('dispatchToState when given an object', function() {
+ it('should use the objects methods as action creators', function() {
+ const dispatch = sinon.spy();
+ const reducer = dispatchToState({
+ test(payload) {
+ return {
+ type: 'FOO',
+ payload: payload,
+ };
+ },
+ });
+
+ const state = reducer(
+ {},
+ {
+ type: 'MELODY/RECEIVE_PROPS',
+ meta: { dispatch },
+ },
+ );
+
+ state.test('hello world');
+ assert(dispatch.calledOnce, 'dispatch was invoked once');
+ assert(
+ dispatch.calledWith({ type: 'FOO', payload: 'hello world' }),
+ 'dispatch was invoked with correct action',
+ );
+ });
+
+ it('should ignore non-function properties', function() {
+ const dispatch = sinon.spy();
+ const reducer = dispatchToState({
+ hello: 'foo',
+ test(payload) {
+ return {
+ type: 'FOO',
+ payload: payload,
+ };
+ },
+ });
+
+ const state = reducer(
+ {},
+ {
+ type: 'MELODY/RECEIVE_PROPS',
+ meta: { dispatch },
+ },
+ );
+
+ state.test('hello world');
+ assert(
+ typeof state.hello === 'undefined',
+ 'hello should not be copied over',
+ );
+ });
+ });
+
+ describe('dispatchToState with props dependency', function() {
+ it('should invoke the dispatch mapper every time props change', function() {
+ let counter = 0;
+ const dispatchMapper = (dispatch, props) => {
+ counter++;
+ return {
+ test() {
+ dispatch({ type: 'test', payload: props.val });
+ },
+ };
+ };
+ const reducer = dispatchToState(dispatchMapper);
+ const dispatch = sinon.spy();
+
+ let state = reducer(
+ {},
+ {
+ type: 'MELODY/RECEIVE_PROPS',
+ meta: { dispatch },
+ payload: { val: 21 },
+ },
+ );
+ assert.equal(counter, 1, 'dispatchMapper called once');
+ state.test();
+ assert(
+ dispatch.calledWith({
+ type: 'test',
+ payload: 21,
+ }),
+ 'dispatch called with a payload of 21',
+ );
+
+ state = reducer(state, {
+ type: 'MELODY/RECEIVE_PROPS',
+ meta: { dispatch },
+ payload: { val: 42 },
+ });
+ assert.equal(counter, 2, 'dispatchMapper called twice');
+ state.test();
+ assert(
+ dispatch.calledWith({
+ type: 'test',
+ payload: 42,
+ }),
+ 'dispatch called with a payload of 21',
+ );
+
+ state = reducer(state, {
+ type: 'fun',
+ payload: { val: 42 },
+ });
+ assert.equal(counter, 2, 'dispatchMapper called twice');
+ state.test();
+ assert(
+ dispatch.calledWith({
+ type: 'test',
+ payload: 42,
+ }),
+ 'dispatch called with a payload of 21',
+ );
+ });
+ });
+ describe('exposeToState', function() {
+ const initialState = { foo: false };
+ const reducer = (state = initialState, { type, payload }) => {
+ if (type === RECEIVE_PROPS) {
+ return Object.assign({}, state, payload);
+ }
+ if (type === 'TOGGLE') {
+ return Object.assign({}, state, {
+ foo: !state.foo,
+ });
+ }
+ return state;
+ };
+
+ const log = [];
+ const component = {
+ handleToggle() {
+ log.push(this);
+ },
+ doNotExpose() {},
+ notAFunction: 1337,
+ };
+
+ it('should expose given functions to the state', function() {
+ const finalReducer = exposeToState(['handleToggle'], reducer);
+ const state = finalReducer(
+ {},
+ {
+ type: RECEIVE_PROPS,
+ meta: component,
+ payload: {
+ qux: 'bar',
+ },
+ },
+ );
+
+ assert(state.qux === 'bar', 'passes props to state');
+ assert(
+ typeof state.handleToggle === 'function',
+ 'handleToggle is a function',
+ );
+ assert(
+ typeof state.doNotExpose === 'undefined',
+ 'only expose given functions',
+ );
+
+ state.handleToggle();
+ assert.deepEqual(
+ log,
+ [component],
+ 'handleToggle is bound to the component',
+ );
+
+ const state2 = finalReducer(state, {
+ type: RECEIVE_PROPS,
+ meta: component,
+ payload: {
+ qux: 'bar',
+ },
+ });
+ assert(
+ state.handleToggle === state2.handleToggle,
+ 'bound functions get memoized',
+ );
+ });
+ it('should take a default reducer if no reducer was given', function() {
+ const finalReducer = exposeToState(['handleToggle']);
+ const state = finalReducer(
+ {},
+ {
+ type: RECEIVE_PROPS,
+ meta: component,
+ payload: {
+ qux: 'bar',
+ },
+ },
+ );
+ assert(state.qux === 'bar', 'passes props to state');
+ assert(
+ typeof state.handleToggle === 'function',
+ 'handleToggle is a function',
+ );
+ });
+ it('should take a ignore actions not known by the default reducer', function() {
+ const finalReducer = exposeToState(['handleToggle']);
+ const givenState = {};
+ const state = finalReducer(givenState, {
+ type: 'foo',
+ meta: component,
+ payload: {
+ qux: 'bar',
+ },
+ });
+ assert.equal(state, givenState);
+ });
+ it('should be able to deal with an undefined state in the default reducer even if it does not know the action type', function() {
+ const finalReducer = exposeToState(['handleToggle']);
+ const state = finalReducer(undefined, {
+ type: 'foo',
+ meta: component,
+ payload: {
+ qux: 'bar',
+ },
+ });
+ expect(state).toEqual({});
+ });
+ it('should be able to deal with an undefined state in the default reducer', function() {
+ const finalReducer = exposeToState(['handleToggle']);
+ const state = finalReducer(undefined, {
+ type: RECEIVE_PROPS,
+ meta: component,
+ payload: {
+ qux: 'bar',
+ },
+ });
+ assert(state.qux === 'bar', 'passes props to state');
+ assert(
+ typeof state.handleToggle === 'function',
+ 'handleToggle is a function',
+ );
+ });
+ it('should throw if given property is not a function', function() {
+ const finalReducer = exposeToState(['notAFunction'], reducer);
+ assert.throws(
+ () =>
+ finalReducer(
+ {},
+ { type: RECEIVE_PROPS, meta: component, payload: {} },
+ ),
+ 'Property `notAFunction` is not a function. Only functions can be exposed to the state.',
+ );
+ });
+ it('should throw if given property is was not found', function() {
+ const finalReducer = exposeToState(['notFound'], reducer);
+ assert.throws(
+ () =>
+ finalReducer(
+ {},
+ { type: RECEIVE_PROPS, meta: component, payload: {} },
+ ),
+ 'Property `notFound` was not found on the component.',
+ );
+ });
+ });
+});
diff --git a/packages/melody-util/package.json b/packages/melody-util/package.json
new file mode 100644
index 0000000..a2873ec
--- /dev/null
+++ b/packages/melody-util/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "melody-util",
+ "version": "0.11.1-rc.1",
+ "description": "",
+ "main": "./lib/index.js",
+ "jsnext:main": "./src/index.js",
+ "scripts": {
+ "build": "mkdir lib; rollup -c ../../rollup.config.js -i src/index.js -o lib/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "melody-component": "^0.11.1-rc.1"
+ },
+ "devDependencies": {
+ "melody-idom": "^0.11.1-rc.1",
+ "rollup-plugin-babel": "^2.6.1"
+ }
+}
diff --git a/packages/melody-util/src/index.js b/packages/melody-util/src/index.js
new file mode 100644
index 0000000..c8223d3
--- /dev/null
+++ b/packages/melody-util/src/index.js
@@ -0,0 +1,189 @@
+/**
+ * Copyright 2017 trivago N.V.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS-IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { RECEIVE_PROPS } from 'melody-component';
+/**
+ * Wraps a series of redux compatible middlewares into a Melody Component Mixin.
+ *
+ * @param {...Function} middlewares
+ */
+export const applyMiddleware = (...middlewares) => proto => {
+ const componentDidInitialize = proto.componentDidInitialize;
+ return {
+ componentDidInitialize() {
+ componentDidInitialize.call(this);
+ // and dispatch
+ let next = this.dispatch;
+ let i = middlewares.length;
+ this.dispatch = action => {
+ return next(action);
+ };
+ while (i--) {
+ const curr = middlewares[i];
+ if (curr) {
+ next = curr(this)(next);
+ }
+ }
+ },
+ };
+};
+
+/**
+ * Wraps an object into a reducer function so that each property of the object should
+ * correspond to the action type that it handles.
+ *
+ * @param {Object} pattern The object that should be used to delegate to the correct reducer.
+ * @param {Object?} initialState An optional state that should be used on initialization.
+ */
+export const createActionReducer = (pattern, initialState) => (
+ state,
+ action,
+) => {
+ if (action.type && pattern[action.type]) {
+ return (
+ (0, pattern[action.type])(state || initialState, action) ||
+ state ||
+ initialState
+ );
+ }
+ return state || initialState;
+};
+
+const mapDispatchToStateWithProps = fn => (state, action) => {
+ if (action.type === 'MELODY/RECEIVE_PROPS') {
+ return Object.assign(
+ {},
+ state,
+ fn(action.meta.dispatch, action.payload),
+ );
+ }
+ return state;
+};
+
+const DISPATCH_PROPS = 'MELODY/DISPATCH_TO_STATE';
+
+const mapDispatchToState = fn => (state, action) => {
+ if (action.type === 'MELODY/RECEIVE_PROPS') {
+ if (state && !state[DISPATCH_PROPS]) {
+ const dispatchMap = fn(action.meta.dispatch);
+ return Object.assign({}, state, dispatchMap, {
+ 'MELODY/DISPATCH_TO_STATE': dispatchMap,
+ });
+ }
+ }
+ return state;
+};
+
+/**
+ * Returns a function that will dispatch the return value of the given action creator
+ * to the given dispatch function.
+ *
+ * Can be used to create an action that is always dispatched to the same store or component.
+ *
+ * @param {Function} action
+ * @param {Function} dispatch
+ */
+export const bindActionToDispatch = (action, dispatch) => (...args) =>
+ dispatch(action(...args));
+
+const wrapObjectToDispatch = dispatchToState => dispatch => {
+ const keys = Object.keys(dispatchToState);
+ const mappedDispatchers = {};
+ let i = 0;
+ for (const len = keys.length; i < len; i++) {
+ const actionCreator = dispatchToState[keys[i]];
+ if (typeof actionCreator === 'function') {
+ mappedDispatchers[keys[i]] = bindActionToDispatch(
+ actionCreator,
+ dispatch,
+ );
+ }
+ }
+ return mappedDispatchers;
+};
+
+/**
+ * Returns a function that can be used to inject dispatchers into the state
+ * of a component.
+ *
+ * Usually used together with [reduce-reducers](https://github.com/acdlite/reduce-reducers).
+ *
+ * @param {Function|Object} dispatchToState The dispatch reducer
+ */
+export function dispatchToState(dispatchToState) {
+ if (typeof dispatchToState === 'object') {
+ return mapDispatchToState(wrapObjectToDispatch(dispatchToState));
+ }
+ const dependsOnProps = dispatchToState.length === 2;
+ if (dependsOnProps) {
+ return mapDispatchToStateWithProps(dispatchToState);
+ } else {
+ return mapDispatchToState(dispatchToState);
+ }
+}
+
+const defaultReducer = (state = {}, { type, payload }) => {
+ if (type === RECEIVE_PROPS) {
+ return Object.assign({}, state, payload);
+ }
+ return state;
+};
+
+/**
+ * Returns a reducer that exposes certain functions of a component to the state
+ *
+ * @param {Array} list List of property names that should be exposed to the state
+ * @param {Function} reducer Component reducer that will be wrapped
+ */
+export function exposeToState(list = [], reducer = defaultReducer) {
+ return (state, action) => {
+ const { type, meta } = action;
+ const result = reducer(state, action);
+
+ // Check if functions have already been merged to state;
+ let hasExpose = true;
+ for (let i = 0, l = list.length; i < l; i++) {
+ const key = list[i];
+ if (!result[key]) {
+ hasExpose = false;
+ break;
+ }
+ }
+
+ if (hasExpose || type !== RECEIVE_PROPS) {
+ return result;
+ }
+
+ const expose = list.reduce((acc, key) => {
+ const prop = meta[key];
+ if (!prop) {
+ throw new Error(
+ 'Property `' + key + '` was not found on the component.',
+ );
+ }
+ if (typeof prop !== 'function') {
+ throw new Error(
+ 'Property `' +
+ key +
+ '` is not a function. Only functions can be exposed to the state.',
+ );
+ }
+ acc[key] = prop.bind(meta);
+ return acc;
+ }, {});
+
+ return Object.assign({}, result, expose);
+ };
+}
diff --git a/rfcs/0000-template.md b/rfcs/0000-template.md
new file mode 100644
index 0000000..336cd00
--- /dev/null
+++ b/rfcs/0000-template.md
@@ -0,0 +1,54 @@
+- Start Date: (fill me in with today's date, YYYY-MM-DD)
+- RFC PR: (leave this empty)
+- Issue: (leave this empty)
+
+# Summary
+
+One paragraph explanation of the feature.
+
+# Motivation
+
+Why are we doing this? What use cases does it support? What is the expected
+outcome?
+
+Please focus on explaining the motivation so that if this RFC is not accepted,
+the motivation could be used to develop alternative solutions. In other words,
+enumerate the constraints you are trying to solve without coupling them too
+closely to the solution you have in mind.
+
+# Detailed design
+
+This is the bulk of the RFC. Explain the design in enough detail for somebody
+familiar with Melody to understand, and for somebody familiar with the
+implementation to implement. This should get into specifics and corner-cases,
+and include examples of how the feature is used. Any new terminology should be
+defined here.
+
+# How We Teach This
+
+What names and terminology work best for these concepts and why? How is this
+idea best presented? As a continuation of existing Redux/React patterns, existing Melody
+patterns, or as a wholly new one?
+
+Would the acceptance of this proposal mean the Melody documentation must be
+re-organized or altered? Does it change how Melody is taught to new users
+at any level?
+
+How should this feature be introduced and taught to existing Melody users?
+
+# Drawbacks
+
+Why should we *not* do this? Please consider the impact on teaching people to
+use Melody, on the integration of this feature with other existing and planned
+features, on the impact of churn on existing users.
+
+There are tradeoffs to choosing any path, please attempt to identify them here.
+
+# Alternatives
+
+What other designs have been considered? What is the impact of not doing this?
+
+# Unresolved questions
+
+Optional, but suggested for first drafts. What parts of the design are still
+TBD?
diff --git a/rfcs/0001-avoid-fail-when-rendering-broken-component.md b/rfcs/0001-avoid-fail-when-rendering-broken-component.md
new file mode 100644
index 0000000..5f15917
--- /dev/null
+++ b/rfcs/0001-avoid-fail-when-rendering-broken-component.md
@@ -0,0 +1,78 @@
+- Start Date: 2016-10-13
+- RFC PR: 0001
+- Issue: XTECH-29
+
+# Summary
+
+Instead of failing the entire rendering process when an error occurs during
+rendering a component, we should warn the user and continue the rendering.
+
+# Motivation
+
+During development, it is inconvenient if the entire rendering process breaks
+just because a single component causes an error.
+
+This feature is supposed to improve the developer experience by clearly showing
+which component failed during rendering and if possible, why it failed.
+
+# Detailed design
+
+Given that we're building Melody for `process.env.NODE_ENV !== 'production'`,
+we would add `try-catch` blocks around the creation and rendering of
+Melody Component. For consistency, we should do the same when calling their
+`componentDidMount` and `componentWillUnmount` lifecycle hooks.
+
+## Instance creation
+
+If an error occurs while creating an instance of a Component, we will render an
+error message instead of the Component.
+
+The error should also be logged to `console.error`.
+
+## Component rendering
+
+If an error occurs while rendering a Component, we will render an error message
+instead of the Component.
+
+The error should also be logged to `console.error`.
+
+**Caution:** We may need to rollback the rendering process to its state before
+the rendering started. To do this, we need to copy the current state and reset
+to it on error.
+
+Once the state has been reset, we'd continue by replacing the Components element
+with the error message.
+
+Crashing the layout (i.e. by rendering a block element where an inline element
+was expected) is not an issue.
+
+We should display the component name, if available, and the error message. Easy
+access to the stack of the error is not stricly needed.
+
+## Errors during lifecycle hook execution
+
+Since the component did render correctly, the best thing we can do for this is
+probably to log the caught error to `console.error`.
+
+# How We Teach This
+
+This feature should be designed to be self-documenting and should not require
+learning effort from our users. Instead, it should help them to understand why
+their code failed.
+
+# Drawbacks
+
+There is a huge performance penalty involved in using a `try-catch` block as part
+of the rendering process, especially in the specified method. Thus, this feature
+can not be used as part of a `production` build.
+
+# Alternatives
+
+Instead of also showing an inline HTML fragment that contains details about the
+error, we could just use the developer console to report the error.
+This could, however, lead to confusion and frustration when a developer does not
+look at the console and instead just wonders why the component does not show up.
+
+# Unresolved questions
+
+None.
\ No newline at end of file
diff --git a/rfcs/README.md b/rfcs/README.md
new file mode 100644
index 0000000..d5820f6
--- /dev/null
+++ b/rfcs/README.md
@@ -0,0 +1,131 @@
+# Melody RFCs
+
+Many changes, including bug fixes and documentation improvements can be
+implemented and reviewed via the normal GitHub pull request workflow.
+
+Some changes though are "substantial", and we ask that these be put
+through a bit of a design process and produce a consensus among the Melody
+core team.
+
+The "RFC" (request for comments) process is intended to provide a
+consistent and controlled path for new features to enter the project.
+
+As a new project, Melody is still **actively developing** this process,
+and it will still change as more features are implemented and the
+community settles on specific approaches to feature development.
+
+## When to follow this process
+
+You should consider using this process if you intend to make "substantial"
+changes to Melody or its documentation. Some examples that would benefit
+from an RFC are:
+
+ - A new feature that creates new API surface area.
+ - The removal of features that already shipped as part of the release
+ channel.
+ - The introduction of new idiomatic usage or conventions, even if they
+ do not include code changes to Melody itself.
+
+The RFC process is a great opportunity to get more eyeballs on your proposal
+before it becomes a part of a released version of Melody. Quite often, even
+proposals that seem "obvious" can be significantly improved once a wider
+group of interested people have a chance to weigh in.
+
+The RFC process can also be helpful to encourage discussions about a proposed
+feature as it is being designed, and incorporate important constraints into
+the design while it's easier to change, before the design has been fully
+implemented.
+
+Some changes do not require an RFC:
+
+ - Rephrasing, reorganizing or refactoring
+ - Addition or removal of warnings
+ - Additions that strictly improve objective, numerical quality
+criteria (speedup, better browser support)
+ - Additions only likely to be _noticed by_ other implementors-of-Melody,
+invisible to users-of-Melody.
+
+## Gathering feedback before submitting
+
+It's often helpful to get feedback on your concept before diving into the
+level of API design detail required for an RFC. **You may open an
+issue on this repo to start a high-level discussion**, with the goal of
+eventually formulating an RFC pull request with the specific implementation
+design.
+
+## What the process is
+
+In short, to get a major feature added to Melody, one usually first gets
+the RFC merged into the RFC repo as a markdown file. At that point the RFC
+is 'active' and may be implemented with the goal of eventual inclusion
+into Melody.
+
+* Clone the Melody Repository
+* Copy `rfcs/0000-template.md` to `rfcs/0000-my-feature.md` (where
+'my-feature' is descriptive. don't assign an RFC number yet).
+* Fill in the RFC. Put care into the details: **RFCs that do not
+present convincing motivation, demonstrate understanding of the
+impact of the design, or are disingenuous about the drawbacks or
+alternatives tend to be poorly-received**.
+* Submit a pull request. As a pull request the RFC will receive design
+feedback from the larger community, and the author should be prepared
+to revise it in response.
+* Build consensus and integrate feedback. RFCs that have broad support
+are much more likely to make progress than those that don't receive any
+comments.
+* Eventually, the team will decide whether the RFC is a candidate
+for inclusion in Melody.
+* RFCs that are candidates for inclusion in Melody will enter a "final comment
+period" lasting 7 days. The beginning of this period will be signaled with a
+comment and tag on the RFC's pull request.
+* An RFC can be modified based upon feedback from the team and community.
+Significant modifications may trigger a new final comment period.
+* An RFC may be rejected by the team after public discussion has settled
+and comments have been made summarizing the rationale for rejection. A member of
+the team should then close the RFC's associated pull request.
+* An RFC may be accepted at the close of its final comment period. A team
+member will merge the RFC's associated pull request, at which point the RFC will
+become 'active'.
+
+## The RFC life-cycle
+
+Once an RFC becomes active, then authors may implement it and submit the
+feature as a pull request to the Melody repo. Becoming 'active' is not a rubber
+stamp, and in particular still does not mean the feature will ultimately
+be merged; it does mean that the core team has agreed to it in principle
+and are amenable to merging it.
+
+Furthermore, the fact that a given RFC has been accepted and is
+'active' implies nothing about what priority is assigned to its
+implementation, nor whether anybody is currently working on it.
+
+Modifications to active RFC's can be done in followup PR's. We strive
+to write each RFC in a manner that it will reflect the final design of
+the feature; but the nature of the process means that we cannot expect
+every merged RFC to actually reflect what the end result will be at
+the time of the next major release; therefore we try to keep each RFC
+document somewhat in sync with the language feature as planned,
+tracking such changes via followup pull requests to the document.
+
+## Implementing an RFC
+
+The author of an RFC is not obligated to implement it. Of course, the
+RFC author (like any other developer) is welcome to post an
+implementation for review after the RFC has been accepted.
+
+If you are interested in working on the implementation for an 'active'
+RFC, but cannot determine if someone else is already working on it,
+feel free to ask (e.g. by leaving a comment on the associated issue).
+
+## Reviewing RFC's
+
+Each week the team will attempt to review some set of open RFC
+pull requests.
+
+**Melody's RFC process owes its inspiration to the [Yarn RFC process], [Rust RFC process] and the [Ember RFC process]**
+
+In fact, this document has been copy and pasted (with minor changes) from Yarn.
+
+[Yarn RFC process]: https://github.com/yarnpgk/rfcs
+[Rust RFC process]: https://github.com/rust-lang/rfcs
+[Ember RFC process]: https://github.com/emberjs/rfcs
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..4fe6706
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,20 @@
+import babel from 'rollup-plugin-babel';
+import json from 'rollup-plugin-json';
+import uglify from 'rollup-plugin-uglify';
+
+var plugins = [json(), babel()];
+
+if (process.env.NODE_ENV === 'production') {
+ plugins.push(
+ uglify({
+ mangle: {
+ toplevel: true,
+ },
+ }),
+ );
+}
+
+export default {
+ format: 'cjs',
+ plugins: plugins,
+};
diff --git a/testsetup/document.createRange.js b/testsetup/document.createRange.js
new file mode 100644
index 0000000..6ddab53
--- /dev/null
+++ b/testsetup/document.createRange.js
@@ -0,0 +1,15 @@
+if (!document.createRange) {
+ document.createRange = function() {
+ return {
+ selectNode(node) {
+ // noop
+ },
+
+ createContextualFragment(html) {
+ const el = document.createElement('div');
+ el.innerHTML = html.trim();
+ return { childNodes: el.childNodes };
+ }
+ };
+ };
+}
diff --git a/testsetup/melody-transform.js b/testsetup/melody-transform.js
new file mode 100644
index 0000000..9bc3217
--- /dev/null
+++ b/testsetup/melody-transform.js
@@ -0,0 +1,14 @@
+const { transformer } = require('../packages/melody-jest-transform');
+
+// melody plugins
+const { extension } = require('../packages/melody-extension-core');
+const idom = require('../packages/melody-plugin-idom');
+
+const customConfig = {
+ plugins: [extension, idom],
+};
+
+// Don't change the name/signature of this function
+exports.process = function(src, path) {
+ return transformer(src, path, customConfig);
+};
diff --git a/testsetup/requestAnimationFrame.js b/testsetup/requestAnimationFrame.js
new file mode 100644
index 0000000..bb79e34
--- /dev/null
+++ b/testsetup/requestAnimationFrame.js
@@ -0,0 +1,34 @@
+// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
+// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
+
+// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
+
+// MIT license
+
+(function() {
+ var lastTime = 0;
+ var vendors = ['ms', 'moz', 'webkit', 'o'];
+ for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
+ window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame']
+ || window[vendors[x] + 'CancelRequestAnimationFrame'];
+ }
+
+ if (!window.requestAnimationFrame) {
+ window.requestAnimationFrame = function(callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function() {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+ }
+
+ if (!window.cancelAnimationFrame) {
+ window.cancelAnimationFrame = function(id) {
+ clearTimeout(id);
+ };
+ }
+}());
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..557e8d7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "target": "es5",
+ "alwaysStrict": true,
+ "declaration": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUnusedLocals": true,
+ "pretty": true,
+ "module": "es6",
+ "moduleResolution": "node",
+ "lib": [
+ "es2017",
+ "dom",
+ "scripthost"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000..432cb52
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,5633 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@types/jest@^20.0.6":
+ version "20.0.8"
+ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-20.0.8.tgz#7f8c97f73d20d3bf5448fbe33661a342002b5954"
+
+"@types/lodash@^4.14.54":
+ version "4.14.77"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.77.tgz#0bc699413e84d6ed5d927ca30ea0f0a890b42d75"
+
+"@types/node@^7.0.7":
+ version "7.0.44"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.44.tgz#20422b3dada536f35bf318ac3e24b8c48200803d"
+
+JSONStream@^1.0.4:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a"
+ dependencies:
+ jsonparse "^1.2.0"
+ through ">=2.2.7 <3"
+
+abab@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+
+acorn-globals@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"
+ dependencies:
+ acorn "^4.0.4"
+
+acorn-jsx@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+ dependencies:
+ acorn "^3.0.4"
+
+acorn@^3.0.4:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+
+acorn@^4.0.4:
+ version "4.0.13"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
+
+acorn@^5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7"
+
+add-stream@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa"
+
+ajv-keywords@^1.0.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
+
+ajv@^4.7.0, ajv@^4.9.1:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+ajv@^5.1.0:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ json-schema-traverse "^0.3.0"
+ json-stable-stringify "^1.0.1"
+
+align-text@^0.1.1, align-text@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
+ dependencies:
+ kind-of "^3.0.2"
+ longest "^1.0.1"
+ repeat-string "^1.5.2"
+
+amdefine@>=0.0.4:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
+
+ansi-escapes@^1.0.0, ansi-escapes@^1.1.0, ansi-escapes@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
+
+ansi-escapes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92"
+
+ansi-regex@^2.0.0, ansi-regex@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+
+ansi-styles@^3.0.0, ansi-styles@^3.1.0, ansi-styles@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@^1.3.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
+ dependencies:
+ micromatch "^2.1.5"
+ normalize-path "^2.0.0"
+
+app-root-path@2.0.1, app-root-path@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46"
+
+append-transform@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
+ dependencies:
+ default-require-extensions "^1.0.0"
+
+aproba@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+
+are-we-there-yet@~1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+argparse@^1.0.7:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-flatten@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+
+array-find-index@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+
+array-ify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece"
+
+array-union@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+ dependencies:
+ array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+
+arrify@^1.0.0, arrify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+asn1.js@^4.0.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40"
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
+assert-plus@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
+
+assert@^1.1.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+ dependencies:
+ util "0.10.3"
+
+assertion-error@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c"
+
+async-each@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+
+async@^1.4.0, async@^1.5.0:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+
+async@^2.1.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
+ dependencies:
+ lodash "^4.14.0"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+aws-sign2@~0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+
+aws4@^1.2.1, aws4@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+
+axios@0.15.3:
+ version "0.15.3"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.15.3.tgz#2c9d638b2e191a08ea1d6cc988eadd6ba5bdc053"
+ dependencies:
+ follow-redirects "1.0.0"
+
+axios@^0.16.1:
+ version "0.16.2"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d"
+ dependencies:
+ follow-redirects "^1.2.3"
+ is-buffer "^1.1.5"
+
+babel-cli@^6.23.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.26.0.tgz#502ab54874d7db88ad00b887a06383ce03d002f1"
+ dependencies:
+ babel-core "^6.26.0"
+ babel-polyfill "^6.26.0"
+ babel-register "^6.26.0"
+ babel-runtime "^6.26.0"
+ commander "^2.11.0"
+ convert-source-map "^1.5.0"
+ fs-readdir-recursive "^1.0.0"
+ glob "^7.1.2"
+ lodash "^4.17.4"
+ output-file-sync "^1.1.2"
+ path-is-absolute "^1.0.1"
+ slash "^1.0.0"
+ source-map "^0.5.6"
+ v8flags "^2.1.1"
+ optionalDependencies:
+ chokidar "^1.6.1"
+
+babel-code-frame@^6.16.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+ dependencies:
+ chalk "^1.1.3"
+ esutils "^2.0.2"
+ js-tokens "^3.0.2"
+
+babel-core@6, babel-core@^6.0.0, babel-core@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-generator "^6.26.0"
+ babel-helpers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-register "^6.26.0"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ convert-source-map "^1.5.0"
+ debug "^2.6.8"
+ json5 "^0.5.1"
+ lodash "^4.17.4"
+ minimatch "^3.0.4"
+ path-is-absolute "^1.0.1"
+ private "^0.1.7"
+ slash "^1.0.0"
+ source-map "^0.5.6"
+
+babel-eslint@^7.1.1:
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.3.tgz#b2fe2d80126470f5c19442dc757253a897710827"
+ dependencies:
+ babel-code-frame "^6.22.0"
+ babel-traverse "^6.23.1"
+ babel-types "^6.23.0"
+ babylon "^6.17.0"
+
+babel-generator@^6.18.0, babel-generator@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5"
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.17.4"
+ source-map "^0.5.6"
+ trim-right "^1.0.1"
+
+babel-helper-bindify-decorators@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664"
+ dependencies:
+ babel-helper-explode-assignable-expression "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-builder-react-jsx@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz#39ff8313b75c8b65dceff1f31d383e0ff2a408a0"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ esutils "^2.0.2"
+
+babel-helper-call-delegate@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-define-map@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-helper-explode-assignable-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-explode-class@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb"
+ dependencies:
+ babel-helper-bindify-decorators "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
+ dependencies:
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-get-function-arity@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-hoist-variables@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-optimise-call-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-regex@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-helper-remap-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-replace-supers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
+ dependencies:
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-jest@20.0.3, babel-jest@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-20.0.3.tgz#e4a03b13dc10389e140fc645d09ffc4ced301671"
+ dependencies:
+ babel-core "^6.0.0"
+ babel-plugin-istanbul "^4.0.0"
+ babel-preset-jest "^20.0.3"
+
+babel-jest@^19.0.0:
+ version "19.0.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-19.0.0.tgz#59323ced99a3a84d359da219ca881074ffc6ce3f"
+ dependencies:
+ babel-core "^6.0.0"
+ babel-plugin-istanbul "^4.0.0"
+ babel-preset-jest "^19.0.0"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-check-es2015-constants@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-external-helpers@^6.5.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-external-helpers/-/babel-plugin-external-helpers-6.22.0.tgz#2285f48b02bd5dede85175caf8c62e86adccefa1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-istanbul@^4.0.0:
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e"
+ dependencies:
+ find-up "^2.1.0"
+ istanbul-lib-instrument "^1.7.5"
+ test-exclude "^4.1.1"
+
+babel-plugin-jest-hoist@^19.0.0:
+ version "19.0.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-19.0.0.tgz#4ae2a04ea612a6e73651f3fde52c178991304bea"
+
+babel-plugin-jest-hoist@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz#afedc853bd3f8dc3548ea671fbe69d03cc2c1767"
+
+babel-plugin-lodash@^3.2.11:
+ version "3.2.11"
+ resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.2.11.tgz#21c8fdec9fe1835efaa737873e3902bdd66d5701"
+ dependencies:
+ glob "^7.1.1"
+ lodash "^4.17.2"
+
+babel-plugin-syntax-async-functions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
+
+babel-plugin-syntax-async-generators@^6.5.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a"
+
+babel-plugin-syntax-class-constructor-call@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416"
+
+babel-plugin-syntax-class-properties@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
+
+babel-plugin-syntax-decorators@^6.13.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b"
+
+babel-plugin-syntax-dynamic-import@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da"
+
+babel-plugin-syntax-exponentiation-operator@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
+
+babel-plugin-syntax-export-extensions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721"
+
+babel-plugin-syntax-flow@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d"
+
+babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+
+babel-plugin-syntax-object-rest-spread@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
+
+babel-plugin-syntax-trailing-function-commas@^6.22.0, babel-plugin-syntax-trailing-function-commas@^6.5.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
+
+babel-plugin-transform-async-generator-functions@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.24.1"
+ babel-plugin-syntax-async-generators "^6.5.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.24.1"
+ babel-plugin-syntax-async-functions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-class-constructor-call@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9"
+ dependencies:
+ babel-plugin-syntax-class-constructor-call "^6.18.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-class-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-plugin-syntax-class-properties "^6.8.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-decorators@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d"
+ dependencies:
+ babel-helper-explode-class "^6.24.1"
+ babel-plugin-syntax-decorators "^6.13.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-arrow-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoping@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-plugin-transform-es2015-classes@^6.24.1, babel-plugin-transform-es2015-classes@^6.9.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
+ dependencies:
+ babel-helper-define-map "^6.24.1"
+ babel-helper-function-name "^6.24.1"
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-helper-replace-supers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-computed-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-destructuring@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-duplicate-keys@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-for-of@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-modules-amd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
+ dependencies:
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-commonjs@^6.24.1, babel-plugin-transform-es2015-modules-commonjs@^6.7.4:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a"
+ dependencies:
+ babel-plugin-transform-strict-mode "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-types "^6.26.0"
+
+babel-plugin-transform-es2015-modules-systemjs@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-umd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
+ dependencies:
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-object-super@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
+ dependencies:
+ babel-helper-replace-supers "^6.24.1"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-parameters@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
+ dependencies:
+ babel-helper-call-delegate "^6.24.1"
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-shorthand-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-spread@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-sticky-regex@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-template-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-typeof-symbol@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-unicode-regex@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ regexpu-core "^2.0.0"
+
+babel-plugin-transform-exponentiation-operator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
+ dependencies:
+ babel-helper-builder-binary-assignment-operator-visitor "^6.24.1"
+ babel-plugin-syntax-exponentiation-operator "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-export-extensions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653"
+ dependencies:
+ babel-plugin-syntax-export-extensions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-flow-strip-types@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf"
+ dependencies:
+ babel-plugin-syntax-flow "^6.18.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-inline-environment-variables@^6.8.0:
+ version "6.8.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-environment-variables/-/babel-plugin-transform-inline-environment-variables-6.8.0.tgz#fc91dd08127dc6c2abdfd1721b11e9602a69ba10"
+ dependencies:
+ babel-runtime "^6.0.0"
+
+babel-plugin-transform-object-rest-spread@^6.22.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
+ dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.8.0"
+ babel-runtime "^6.26.0"
+
+babel-plugin-transform-react-display-name@^6.23.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx-self@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx-source@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz#66ac12153f5cd2d17b3c19268f4bf0197f44ecd6"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz#840a028e7df460dfc3a2d29f0c0d91f6376e66a3"
+ dependencies:
+ babel-helper-builder-react-jsx "^6.24.1"
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-regenerator@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
+ dependencies:
+ regenerator-transform "^0.10.0"
+
+babel-plugin-transform-runtime@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz#88490d446502ea9b8e7efb0fe09ec4d99479b1ee"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-strict-mode@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-polyfill@^6.16.0, babel-polyfill@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153"
+ dependencies:
+ babel-runtime "^6.26.0"
+ core-js "^2.5.0"
+ regenerator-runtime "^0.10.5"
+
+babel-preset-es2015-loose@8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2015-loose/-/babel-preset-es2015-loose-8.0.0.tgz#82ee293ab31329c7a94686b644b62adfbcdc3f57"
+ dependencies:
+ modify-babel-preset "^3.1.0"
+
+babel-preset-es2015-minimal-rollup@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2015-minimal-rollup/-/babel-preset-es2015-minimal-rollup-2.1.1.tgz#14927e6c20e5ba75638425992df49a9aab450ee2"
+ dependencies:
+ babel-plugin-external-helpers "^6.5.0"
+ modify-babel-preset "^3.2.1"
+
+babel-preset-es2015@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939"
+ dependencies:
+ babel-plugin-check-es2015-constants "^6.22.0"
+ babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.24.1"
+ babel-plugin-transform-es2015-classes "^6.24.1"
+ babel-plugin-transform-es2015-computed-properties "^6.24.1"
+ babel-plugin-transform-es2015-destructuring "^6.22.0"
+ babel-plugin-transform-es2015-duplicate-keys "^6.24.1"
+ babel-plugin-transform-es2015-for-of "^6.22.0"
+ babel-plugin-transform-es2015-function-name "^6.24.1"
+ babel-plugin-transform-es2015-literals "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-plugin-transform-es2015-modules-systemjs "^6.24.1"
+ babel-plugin-transform-es2015-modules-umd "^6.24.1"
+ babel-plugin-transform-es2015-object-super "^6.24.1"
+ babel-plugin-transform-es2015-parameters "^6.24.1"
+ babel-plugin-transform-es2015-shorthand-properties "^6.24.1"
+ babel-plugin-transform-es2015-spread "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.24.1"
+ babel-plugin-transform-es2015-template-literals "^6.22.0"
+ babel-plugin-transform-es2015-typeof-symbol "^6.22.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.24.1"
+ babel-plugin-transform-regenerator "^6.24.1"
+
+babel-preset-flow@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz#e71218887085ae9a24b5be4169affb599816c49d"
+ dependencies:
+ babel-plugin-transform-flow-strip-types "^6.22.0"
+
+babel-preset-jest@^19.0.0:
+ version "19.0.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-19.0.0.tgz#22d67201d02324a195811288eb38294bb3cac396"
+ dependencies:
+ babel-plugin-jest-hoist "^19.0.0"
+
+babel-preset-jest@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-20.0.3.tgz#cbacaadecb5d689ca1e1de1360ebfc66862c178a"
+ dependencies:
+ babel-plugin-jest-hoist "^20.0.3"
+
+babel-preset-node6@^11.0.0:
+ version "11.0.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-node6/-/babel-preset-node6-11.0.0.tgz#0835994517248985a29d18f6d465dab16bb8a7d8"
+ dependencies:
+ babel-plugin-syntax-trailing-function-commas "^6.5.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.7.4"
+
+babel-preset-react@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.24.1.tgz#ba69dfaea45fc3ec639b6a4ecea6e17702c91380"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.3.13"
+ babel-plugin-transform-react-display-name "^6.23.0"
+ babel-plugin-transform-react-jsx "^6.24.1"
+ babel-plugin-transform-react-jsx-self "^6.22.0"
+ babel-plugin-transform-react-jsx-source "^6.22.0"
+ babel-preset-flow "^6.23.0"
+
+babel-preset-stage-1@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0"
+ dependencies:
+ babel-plugin-transform-class-constructor-call "^6.24.1"
+ babel-plugin-transform-export-extensions "^6.22.0"
+ babel-preset-stage-2 "^6.24.1"
+
+babel-preset-stage-2@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1"
+ dependencies:
+ babel-plugin-syntax-dynamic-import "^6.18.0"
+ babel-plugin-transform-class-properties "^6.24.1"
+ babel-plugin-transform-decorators "^6.24.1"
+ babel-preset-stage-3 "^6.24.1"
+
+babel-preset-stage-3@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395"
+ dependencies:
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-generator-functions "^6.24.1"
+ babel-plugin-transform-async-to-generator "^6.24.1"
+ babel-plugin-transform-exponentiation-operator "^6.24.1"
+ babel-plugin-transform-object-rest-spread "^6.22.0"
+
+babel-register@^6.23.0, babel-register@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
+ dependencies:
+ babel-core "^6.26.0"
+ babel-runtime "^6.26.0"
+ core-js "^2.5.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.17.4"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.15"
+
+babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.11.0"
+
+babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ lodash "^4.17.4"
+
+babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ debug "^2.6.8"
+ globals "^9.18.0"
+ invariant "^2.2.2"
+ lodash "^4.17.4"
+
+babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
+ dependencies:
+ babel-runtime "^6.26.0"
+ esutils "^2.0.2"
+ lodash "^4.17.4"
+ to-fast-properties "^1.0.3"
+
+babylon@^6.17.0, babylon@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+base64-js@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+binary-extensions@^1.0.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0"
+
+block-stream@*:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+ dependencies:
+ inherits "~2.0.0"
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+
+boom@2.x.x:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
+ dependencies:
+ hoek "2.x.x"
+
+boom@4.x.x:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
+ dependencies:
+ hoek "4.x.x"
+
+boom@5.x.x:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02"
+ dependencies:
+ hoek "4.x.x"
+
+brace-expansion@^1.1.7:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+brorand@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+
+browser-resolve@^1.11.2:
+ version "1.11.2"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce"
+ dependencies:
+ resolve "1.1.7"
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.0.tgz#1d2ad62a8b479f23f0ab631c1be86a82dbccbe48"
+ dependencies:
+ buffer-xor "^1.0.3"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.3"
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a"
+ dependencies:
+ browserify-aes "^1.0.4"
+ browserify-des "^1.0.0"
+ evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd"
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+
+browserify-rsa@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+ dependencies:
+ bn.js "^4.1.0"
+ randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+ dependencies:
+ bn.js "^4.1.1"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.2"
+ elliptic "^6.0.0"
+ inherits "^2.0.1"
+ parse-asn1 "^5.0.0"
+
+browserify-zlib@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d"
+ dependencies:
+ pako "~0.2.0"
+
+bser@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-1.0.2.tgz#381116970b2a6deea5646dd15dd7278444b56169"
+ dependencies:
+ node-int64 "^0.4.0"
+
+bser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
+ dependencies:
+ node-int64 "^0.4.0"
+
+buffer-xor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+
+buffer@^4.3.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+builtin-status-codes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+
+bundlesize@^0.13.2:
+ version "0.13.2"
+ resolved "https://registry.yarnpkg.com/bundlesize/-/bundlesize-0.13.2.tgz#3a8aa61aa9f6d54f63321e2dfc7aa7716f91e73f"
+ dependencies:
+ axios "^0.16.1"
+ bytes "^2.5.0"
+ ci-env "^1.4.0"
+ commander "^2.11.0"
+ github-build "^1.2.0"
+ glob "^7.1.2"
+ gzip-size "^3.0.0"
+ prettycli "^1.3.0"
+ read-pkg-up "^2.0.0"
+
+byline@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
+
+bytes@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a"
+
+cachedir@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-1.1.1.tgz#e1363075ea206a12767d92bb711c8a2f76a10f62"
+ dependencies:
+ os-homedir "^1.0.1"
+
+caller-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
+ dependencies:
+ callsites "^0.2.0"
+
+callsites@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+
+callsites@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+
+camelcase-keys@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+ dependencies:
+ camelcase "^2.0.0"
+ map-obj "^1.0.0"
+
+camelcase@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
+
+camelcase@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+
+camelcase@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+
+camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+center-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
+ dependencies:
+ align-text "^0.1.3"
+ lazy-cache "^1.0.3"
+
+chai-subset@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/chai-subset/-/chai-subset-1.6.0.tgz#a5d0ca14e329a79596ed70058b6646bd6988cfe9"
+
+chai@^3.0.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247"
+ dependencies:
+ assertion-error "^1.0.1"
+ deep-eql "^0.1.3"
+ type-detect "^1.0.0"
+
+chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+chalk@2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e"
+ dependencies:
+ ansi-styles "^3.1.0"
+ escape-string-regexp "^1.0.5"
+ supports-color "^4.0.0"
+
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.2.0.tgz#477b3bf2f9b8fd5ca9e429747e37f724ee7af240"
+ dependencies:
+ ansi-styles "^3.1.0"
+ escape-string-regexp "^1.0.5"
+ supports-color "^4.0.0"
+
+chokidar@^1.6.1:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
+ dependencies:
+ anymatch "^1.3.0"
+ async-each "^1.0.0"
+ glob-parent "^2.0.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^2.0.0"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ optionalDependencies:
+ fsevents "^1.0.0"
+
+ci-env@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/ci-env/-/ci-env-1.4.0.tgz#7e4c4ed1d10cedce734293e04dde94fcdfe74d67"
+
+ci-info@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.1.tgz#47b44df118c48d2597b56d342e7e25791060171a"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+circular-json@^0.3.1:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
+
+cli-cursor@^1.0.1, cli-cursor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
+ dependencies:
+ restore-cursor "^1.0.1"
+
+cli-cursor@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+ dependencies:
+ restore-cursor "^2.0.0"
+
+cli-spinners@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c"
+
+cli-truncate@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"
+ dependencies:
+ slice-ansi "0.0.4"
+ string-width "^1.0.1"
+
+cli-width@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
+
+cliui@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
+ dependencies:
+ center-align "^0.1.1"
+ right-align "^0.1.1"
+ wordwrap "0.0.2"
+
+cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+clone@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
+
+cmd-shim@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb"
+ dependencies:
+ graceful-fs "^4.1.2"
+ mkdirp "~0.5.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+
+color-convert@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
+ dependencies:
+ color-name "^1.1.1"
+
+color-name@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+columnify@^1.5.4:
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb"
+ dependencies:
+ strip-ansi "^3.0.0"
+ wcwidth "^1.0.0"
+
+combined-stream@^1.0.5, combined-stream@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+command-join@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/command-join/-/command-join-2.0.0.tgz#52e8b984f4872d952ff1bdc8b98397d27c7144cf"
+
+commander@^2.11.0, commander@^2.9.0, commander@~2.11.0:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
+
+commitizen@^2.9.6:
+ version "2.9.6"
+ resolved "https://registry.yarnpkg.com/commitizen/-/commitizen-2.9.6.tgz#c0d00535ef264da7f63737edfda4228983fa2291"
+ dependencies:
+ cachedir "^1.1.0"
+ chalk "1.1.3"
+ cz-conventional-changelog "1.2.0"
+ dedent "0.6.0"
+ detect-indent "4.0.0"
+ find-node-modules "1.0.4"
+ find-root "1.0.0"
+ fs-extra "^1.0.0"
+ glob "7.1.1"
+ inquirer "1.2.3"
+ lodash "4.17.2"
+ minimist "1.2.0"
+ path-exists "2.1.0"
+ shelljs "0.7.6"
+ strip-json-comments "2.0.1"
+
+common-tags@^1.3.1:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.4.0.tgz#1187be4f3d4cf0c0427d43f74eef1f73501614c0"
+ dependencies:
+ babel-runtime "^6.18.0"
+
+compare-func@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-1.3.2.tgz#99dd0ba457e1f9bc722b12c08ec33eeab31fa648"
+ dependencies:
+ array-ify "^1.0.0"
+ dot-prop "^3.0.0"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+concat-stream@^1.4.10, concat-stream@^1.4.7, concat-stream@^1.5.2:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+console-browserify@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
+ dependencies:
+ date-now "^0.1.4"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+
+constants-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+
+content-type-parser@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94"
+
+conventional-changelog-angular@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-1.5.1.tgz#974e73aa1c39c392e4364f2952bd9a62904e9ea3"
+ dependencies:
+ compare-func "^1.3.1"
+ q "^1.4.1"
+
+conventional-changelog-atom@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-atom/-/conventional-changelog-atom-0.1.1.tgz#d40a9b297961b53c745e5d1718fd1a3379f6a92f"
+ dependencies:
+ q "^1.4.1"
+
+conventional-changelog-cli@^1.3.2:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-cli/-/conventional-changelog-cli-1.3.4.tgz#38f7ff7ac7bca92ea110897ea08b473f2055a27c"
+ dependencies:
+ add-stream "^1.0.0"
+ conventional-changelog "^1.1.6"
+ lodash "^4.1.0"
+ meow "^3.7.0"
+ tempfile "^1.1.1"
+
+conventional-changelog-codemirror@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-codemirror/-/conventional-changelog-codemirror-0.2.0.tgz#3cc925955f3b14402827b15168049821972d9459"
+ dependencies:
+ q "^1.4.1"
+
+conventional-changelog-core@^1.9.2:
+ version "1.9.2"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-1.9.2.tgz#a09b6b959161671ff45b93cc9efb0444e7c845c0"
+ dependencies:
+ conventional-changelog-writer "^2.0.1"
+ conventional-commits-parser "^2.0.0"
+ dateformat "^1.0.12"
+ get-pkg-repo "^1.0.0"
+ git-raw-commits "^1.2.0"
+ git-remote-origin-url "^2.0.0"
+ git-semver-tags "^1.2.2"
+ lodash "^4.0.0"
+ normalize-package-data "^2.3.5"
+ q "^1.4.1"
+ read-pkg "^1.1.0"
+ read-pkg-up "^1.0.1"
+ through2 "^2.0.0"
+
+conventional-changelog-ember@^0.2.8:
+ version "0.2.8"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-ember/-/conventional-changelog-ember-0.2.8.tgz#65e686da83d23b67133d1f853908c87f948035c0"
+ dependencies:
+ q "^1.4.1"
+
+conventional-changelog-eslint@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-eslint/-/conventional-changelog-eslint-0.2.0.tgz#b4b9b5dc09417844d87c7bcfb16bdcc686c4b1c1"
+ dependencies:
+ q "^1.4.1"
+
+conventional-changelog-express@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-express/-/conventional-changelog-express-0.2.0.tgz#8d666ad41b10ebf964a4602062ddd2e00deb518d"
+ dependencies:
+ q "^1.4.1"
+
+conventional-changelog-jquery@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-jquery/-/conventional-changelog-jquery-0.1.0.tgz#0208397162e3846986e71273b6c79c5b5f80f510"
+ dependencies:
+ q "^1.4.1"
+
+conventional-changelog-jscs@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-jscs/-/conventional-changelog-jscs-0.1.0.tgz#0479eb443cc7d72c58bf0bcf0ef1d444a92f0e5c"
+ dependencies:
+ q "^1.4.1"
+
+conventional-changelog-jshint@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-jshint/-/conventional-changelog-jshint-0.2.0.tgz#63ad7aec66cd1ae559bafe80348c4657a6eb1872"
+ dependencies:
+ compare-func "^1.3.1"
+ q "^1.4.1"
+
+conventional-changelog-writer@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-2.0.1.tgz#47c10d0faba526b78d194389d1e931d09ee62372"
+ dependencies:
+ compare-func "^1.3.1"
+ conventional-commits-filter "^1.0.0"
+ dateformat "^1.0.11"
+ handlebars "^4.0.2"
+ json-stringify-safe "^5.0.1"
+ lodash "^4.0.0"
+ meow "^3.3.0"
+ semver "^5.0.1"
+ split "^1.0.0"
+ through2 "^2.0.0"
+
+conventional-changelog@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-1.1.6.tgz#ebd9b1ab63766c715f903f654626b6b1c0da7762"
+ dependencies:
+ conventional-changelog-angular "^1.5.1"
+ conventional-changelog-atom "^0.1.1"
+ conventional-changelog-codemirror "^0.2.0"
+ conventional-changelog-core "^1.9.2"
+ conventional-changelog-ember "^0.2.8"
+ conventional-changelog-eslint "^0.2.0"
+ conventional-changelog-express "^0.2.0"
+ conventional-changelog-jquery "^0.1.0"
+ conventional-changelog-jscs "^0.1.0"
+ conventional-changelog-jshint "^0.2.0"
+
+conventional-commit-types@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/conventional-commit-types/-/conventional-commit-types-2.2.0.tgz#5db95739d6c212acbe7b6f656a11b940baa68946"
+
+conventional-commits-filter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-1.0.0.tgz#6fc2a659372bc3f2339cf9ffff7e1b0344b93039"
+ dependencies:
+ is-subset "^0.1.1"
+ modify-values "^1.0.0"
+
+conventional-commits-parser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-2.0.0.tgz#71d01910cb0a99aeb20c144e50f81f4df3178447"
+ dependencies:
+ JSONStream "^1.0.4"
+ is-text-path "^1.0.0"
+ lodash "^4.2.1"
+ meow "^3.3.0"
+ split2 "^2.0.0"
+ through2 "^2.0.0"
+ trim-off-newlines "^1.0.0"
+
+conventional-recommended-bump@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-1.0.2.tgz#31856443ab6f9453a1827650e7cc15ec28769645"
+ dependencies:
+ concat-stream "^1.4.10"
+ conventional-commits-filter "^1.0.0"
+ conventional-commits-parser "^2.0.0"
+ git-raw-commits "^1.2.0"
+ git-semver-tags "^1.2.2"
+ meow "^3.3.0"
+ object-assign "^4.0.1"
+
+convert-source-map@^1.4.0, convert-source-map@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
+
+core-js@^2.4.0, core-js@^2.5.0:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+cosmiconfig@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-1.1.0.tgz#0dea0f9804efdfb929fbb1b188e25553ea053d37"
+ dependencies:
+ graceful-fs "^4.1.2"
+ js-yaml "^3.4.3"
+ minimist "^1.2.0"
+ object-assign "^4.0.1"
+ os-homedir "^1.0.1"
+ parse-json "^2.2.0"
+ pinkie-promise "^2.0.0"
+ require-from-string "^1.1.0"
+
+create-ecdh@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
+ dependencies:
+ bn.js "^4.1.0"
+ elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd"
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06"
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+cross-env@^3.2.3:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-3.2.4.tgz#9e0585f277864ed421ce756f81a980ff0d698aba"
+ dependencies:
+ cross-spawn "^5.1.0"
+ is-windows "^1.0.0"
+
+cross-spawn@^5.0.1, cross-spawn@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cryptiles@2.x.x:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
+ dependencies:
+ boom "2.x.x"
+
+cryptiles@3.x.x:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
+ dependencies:
+ boom "5.x.x"
+
+crypto-browserify@^3.11.0:
+ version "3.11.1"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f"
+ dependencies:
+ browserify-cipher "^1.0.0"
+ browserify-sign "^4.0.0"
+ create-ecdh "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.0"
+ diffie-hellman "^5.0.0"
+ inherits "^2.0.1"
+ pbkdf2 "^3.0.3"
+ public-encrypt "^4.0.0"
+ randombytes "^2.0.0"
+
+cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b"
+
+"cssstyle@>= 0.2.37 < 0.3.0":
+ version "0.2.37"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54"
+ dependencies:
+ cssom "0.3.x"
+
+currently-unhandled@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+ dependencies:
+ array-find-index "^1.0.1"
+
+cz-conventional-changelog@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-1.2.0.tgz#2bca04964c8919b23f3fd6a89ef5e6008b31b3f8"
+ dependencies:
+ conventional-commit-types "^2.0.0"
+ lodash.map "^4.5.1"
+ longest "^1.0.1"
+ pad-right "^0.2.2"
+ right-pad "^1.0.1"
+ word-wrap "^1.0.3"
+
+d@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
+ dependencies:
+ es5-ext "^0.10.9"
+
+dargs@^4.0.1:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/dargs/-/dargs-4.1.0.tgz#03a9dbb4b5c2f139bf14ae53f0b8a2a6a86f4e17"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ dependencies:
+ assert-plus "^1.0.0"
+
+date-fns@^1.27.2:
+ version "1.29.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
+
+date-now@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+
+dateformat@^1.0.11, dateformat@^1.0.12:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9"
+ dependencies:
+ get-stdin "^4.0.1"
+ meow "^3.3.0"
+
+debug@^2.1.1, debug@^2.2.0, debug@^2.6.3, debug@^2.6.8, debug@^2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ dependencies:
+ ms "2.0.0"
+
+decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+dedent@0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.6.0.tgz#0e6da8f0ce52838ef5cec5c8f9396b0c1b64a3cb"
+
+dedent@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
+
+deep-eql@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2"
+ dependencies:
+ type-detect "0.1.1"
+
+deep-extend@~0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+default-require-extensions@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
+ dependencies:
+ strip-bom "^2.0.0"
+
+defaults@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
+ dependencies:
+ clone "^1.0.2"
+
+del@^2.0.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
+ dependencies:
+ globby "^5.0.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ rimraf "^2.2.8"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+
+des.js@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
+ dependencies:
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+detect-file@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-0.1.0.tgz#4935dedfd9488648e006b0129566e9386711ea63"
+ dependencies:
+ fs-exists-sync "^0.1.0"
+
+detect-indent@4.0.0, detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ dependencies:
+ repeating "^2.0.0"
+
+detect-indent@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
+
+diff@^3.2.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
+
+diffie-hellman@^5.0.0:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
+ dependencies:
+ bn.js "^4.1.0"
+ miller-rabin "^4.0.0"
+ randombytes "^2.0.0"
+
+doctrine@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.0"
+
+domain-browser@^1.1.1:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
+
+dot-prop@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177"
+ dependencies:
+ is-obj "^1.0.0"
+
+duplexer@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
+ dependencies:
+ jsbn "~0.1.0"
+
+elegant-spinner@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
+
+elliptic@^6.0.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
+ dependencies:
+ bn.js "^4.4.0"
+ brorand "^1.0.1"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.0"
+
+errno@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
+ dependencies:
+ prr "~0.0.0"
+
+error-ex@^1.2.0, error-ex@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
+ version "0.10.35"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.35.tgz#18ee858ce6a3c45c7d79e91c15fcca9ec568494f"
+ dependencies:
+ es6-iterator "~2.0.1"
+ es6-symbol "~3.1.1"
+
+es6-iterator@^2.0.1, es6-iterator@~2.0.1:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+ dependencies:
+ d "1"
+ es5-ext "^0.10.35"
+ es6-symbol "^3.1.1"
+
+es6-map@^0.1.3:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-set "~0.1.5"
+ es6-symbol "~3.1.1"
+ event-emitter "~0.3.5"
+
+es6-set@~0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-symbol "3.1.1"
+ event-emitter "~0.3.5"
+
+es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+es6-weak-map@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
+ dependencies:
+ d "1"
+ es5-ext "^0.10.14"
+ es6-iterator "^2.0.1"
+ es6-symbol "^3.1.1"
+
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+escodegen@^1.6.1:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.0.tgz#9811a2f265dc1cd3894420ee3717064b632b8852"
+ dependencies:
+ esprima "^3.1.3"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.5.6"
+
+escope@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3"
+ dependencies:
+ es6-map "^0.1.3"
+ es6-weak-map "^2.0.1"
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-config-idiomatic@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-idiomatic/-/eslint-config-idiomatic-3.1.0.tgz#66de8313cad5b311d18950a40048678e629857e4"
+
+eslint-config-prettier@^2.3.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-2.6.0.tgz#f21db0ebb438ad678fb98946097c4bb198befccc"
+ dependencies:
+ get-stdin "^5.0.1"
+
+eslint-plugin-prettier@^2.1.2:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.3.1.tgz#e7a746c67e716f335274b88295a9ead9f544e44d"
+ dependencies:
+ fast-diff "^1.1.1"
+ jest-docblock "^21.0.0"
+
+eslint@^3.17.1:
+ version "3.19.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc"
+ dependencies:
+ babel-code-frame "^6.16.0"
+ chalk "^1.1.3"
+ concat-stream "^1.5.2"
+ debug "^2.1.1"
+ doctrine "^2.0.0"
+ escope "^3.6.0"
+ espree "^3.4.0"
+ esquery "^1.0.0"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ file-entry-cache "^2.0.0"
+ glob "^7.0.3"
+ globals "^9.14.0"
+ ignore "^3.2.0"
+ imurmurhash "^0.1.4"
+ inquirer "^0.12.0"
+ is-my-json-valid "^2.10.0"
+ is-resolvable "^1.0.0"
+ js-yaml "^3.5.1"
+ json-stable-stringify "^1.0.0"
+ levn "^0.3.0"
+ lodash "^4.0.0"
+ mkdirp "^0.5.0"
+ natural-compare "^1.4.0"
+ optionator "^0.8.2"
+ path-is-inside "^1.0.1"
+ pluralize "^1.2.1"
+ progress "^1.1.8"
+ require-uncached "^1.0.2"
+ shelljs "^0.7.5"
+ strip-bom "^3.0.0"
+ strip-json-comments "~2.0.1"
+ table "^3.7.8"
+ text-table "~0.2.0"
+ user-home "^2.0.0"
+
+espree@^3.4.0:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.1.tgz#0c988b8ab46db53100a1954ae4ba995ddd27d87e"
+ dependencies:
+ acorn "^5.1.1"
+ acorn-jsx "^3.0.0"
+
+esprima@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+
+esquery@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+ dependencies:
+ estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
+ dependencies:
+ estraverse "^4.1.0"
+ object-assign "^4.0.1"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+estree-walker@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e"
+
+estree-walker@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.3.1.tgz#e6b1a51cf7292524e7237c312e5fe6660c1ce1aa"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+event-emitter@~0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+events@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+ dependencies:
+ md5.js "^1.3.4"
+ safe-buffer "^5.1.1"
+
+exec-sh@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38"
+ dependencies:
+ merge "^1.1.3"
+
+execa@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
+ dependencies:
+ cross-spawn "^5.0.1"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+execa@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da"
+ dependencies:
+ cross-spawn "^5.0.1"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+exit-hook@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ dependencies:
+ fill-range "^2.1.0"
+
+expand-tilde@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449"
+ dependencies:
+ os-homedir "^1.0.1"
+
+extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+
+external-editor@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-1.1.1.tgz#12d7b0db850f7ff7e7081baf4005700060c4600b"
+ dependencies:
+ extend "^3.0.0"
+ spawn-sync "^1.0.15"
+ tmp "^0.0.29"
+
+external-editor@^2.0.4:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.5.tgz#52c249a3981b9ba187c7cacf5beb50bf1d91a6bc"
+ dependencies:
+ iconv-lite "^0.4.17"
+ jschardet "^1.4.2"
+ tmp "^0.0.33"
+
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ dependencies:
+ is-extglob "^1.0.0"
+
+extsprintf@1.3.0, extsprintf@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+
+fast-diff@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154"
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+fb-watchman@^1.8.0:
+ version "1.9.2"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-1.9.2.tgz#a24cf47827f82d38fb59a69ad70b76e3b6ae7383"
+ dependencies:
+ bser "1.0.2"
+
+fb-watchman@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+ dependencies:
+ bser "^2.0.0"
+
+figures@^1.3.5, figures@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+ object-assign "^4.1.0"
+
+figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+file-entry-cache@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
+ dependencies:
+ flat-cache "^1.2.1"
+ object-assign "^4.0.1"
+
+filename-regex@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+
+fileset@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0"
+ dependencies:
+ glob "^7.0.3"
+ minimatch "^3.0.3"
+
+fill-range@^2.1.0:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^1.1.3"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+find-node-modules@1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/find-node-modules/-/find-node-modules-1.0.4.tgz#b6deb3cccb699c87037677bcede2c5f5862b2550"
+ dependencies:
+ findup-sync "0.4.2"
+ merge "^1.2.0"
+
+find-root@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.0.0.tgz#962ff211aab25c6520feeeb8d6287f8f6e95807a"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^2.0.0, find-up@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ dependencies:
+ locate-path "^2.0.0"
+
+findup-sync@0.4.2:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.2.tgz#a8117d0f73124f5a4546839579fe52d7129fb5e5"
+ dependencies:
+ detect-file "^0.1.0"
+ is-glob "^2.0.1"
+ micromatch "^2.3.7"
+ resolve-dir "^0.1.0"
+
+flat-cache@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481"
+ dependencies:
+ circular-json "^0.3.1"
+ del "^2.0.2"
+ graceful-fs "^4.1.2"
+ write "^0.2.1"
+
+flux-standard-action@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/flux-standard-action/-/flux-standard-action-0.6.1.tgz#6f34211b94834ea1c3cc30f4e7afad3d0fbf71a2"
+ dependencies:
+ lodash.isplainobject "^3.2.0"
+
+follow-redirects@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.0.0.tgz#8e34298cbd2e176f254effec75a1c78cc849fd37"
+ dependencies:
+ debug "^2.2.0"
+
+follow-redirects@^1.2.3:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.5.tgz#ffd3e14cbdd5eaa72f61b6368c1f68516c2a26cc"
+ dependencies:
+ debug "^2.6.9"
+
+for-in@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ dependencies:
+ for-in "^1.0.1"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.1.1:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+form-data@~2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+formatio@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9"
+ dependencies:
+ samsam "~1.1"
+
+fs-exists-sync@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add"
+
+fs-extra@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+ klaw "^1.0.0"
+
+fs-extra@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^4.0.0"
+ universalify "^0.1.0"
+
+fs-readdir-recursive@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+fsevents@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4"
+ dependencies:
+ nan "^2.3.0"
+ node-pre-gyp "^0.6.36"
+
+fstream-ignore@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+ dependencies:
+ fstream "^1.0.0"
+ inherits "2"
+ minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+generate-function@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
+
+generate-object-property@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
+ dependencies:
+ is-property "^1.0.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+
+get-own-enumerable-property-symbols@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz#5c4ad87f2834c4b9b4e84549dc1e0650fb38c24b"
+
+get-pkg-repo@^1.0.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ meow "^3.3.0"
+ normalize-package-data "^2.3.0"
+ parse-github-repo-url "^1.3.0"
+ through2 "^2.0.0"
+
+get-port@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc"
+
+get-stdin@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
+
+get-stdin@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
+
+get-stream@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ dependencies:
+ assert-plus "^1.0.0"
+
+git-raw-commits@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-1.2.0.tgz#0f3a8bfd99ae0f2d8b9224d58892975e9a52d03c"
+ dependencies:
+ dargs "^4.0.1"
+ lodash.template "^4.0.2"
+ meow "^3.3.0"
+ split2 "^2.0.0"
+ through2 "^2.0.0"
+
+git-remote-origin-url@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f"
+ dependencies:
+ gitconfiglocal "^1.0.0"
+ pify "^2.3.0"
+
+git-semver-tags@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-1.2.2.tgz#a2139be1bf6e337e125f3eb8bb8fc6f5d4d6445f"
+ dependencies:
+ meow "^3.3.0"
+ semver "^5.0.1"
+
+gitconfiglocal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b"
+ dependencies:
+ ini "^1.3.2"
+
+github-build@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/github-build/-/github-build-1.2.0.tgz#b0bdb705ae4088218577e863c1a301030211051f"
+ dependencies:
+ axios "0.15.3"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ dependencies:
+ is-glob "^2.0.0"
+
+glob-parent@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+ dependencies:
+ is-glob "^3.1.0"
+ path-dirname "^1.0.0"
+
+glob@7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.2"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+global-modules@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d"
+ dependencies:
+ global-prefix "^0.1.4"
+ is-windows "^0.2.0"
+
+global-prefix@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f"
+ dependencies:
+ homedir-polyfill "^1.0.0"
+ ini "^1.3.4"
+ is-windows "^0.2.0"
+ which "^1.2.12"
+
+globals@^9.14.0, globals@^9.18.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
+globby@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
+ dependencies:
+ array-union "^1.0.1"
+ arrify "^1.0.0"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+globby@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
+ dependencies:
+ array-union "^1.0.1"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+
+gzip-size@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
+ dependencies:
+ duplexer "^0.1.1"
+
+handlebars@^4.0.2, handlebars@^4.0.3:
+ version "4.0.11"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
+ dependencies:
+ async "^1.4.0"
+ optimist "^0.6.1"
+ source-map "^0.4.4"
+ optionalDependencies:
+ uglify-js "^2.6"
+
+har-schema@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+
+har-validator@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
+ dependencies:
+ ajv "^4.9.1"
+ har-schema "^1.0.5"
+
+har-validator@~5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
+ dependencies:
+ ajv "^5.1.0"
+ har-schema "^2.0.0"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
+
+has-flag@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+
+hash-base@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
+ dependencies:
+ inherits "^2.0.1"
+
+hash-base@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.0"
+
+hawk@3.1.3, hawk@~3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
+ dependencies:
+ boom "2.x.x"
+ cryptiles "2.x.x"
+ hoek "2.x.x"
+ sntp "1.x.x"
+
+hawk@~6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"
+ dependencies:
+ boom "4.x.x"
+ cryptiles "3.x.x"
+ hoek "4.x.x"
+ sntp "2.x.x"
+
+hmac-drbg@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
+hoek@2.x.x:
+ version "2.16.3"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+
+hoek@4.x.x:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
+
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+homedir-polyfill@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
+ dependencies:
+ parse-passwd "^1.0.0"
+
+hosted-git-info@^2.1.4, hosted-git-info@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
+
+html-encoding-sniffer@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz#79bf7a785ea495fe66165e734153f363ff5437da"
+ dependencies:
+ whatwg-encoding "^1.0.1"
+
+http-signature@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
+ dependencies:
+ assert-plus "^0.2.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+https-browserify@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
+
+husky@^0.14.3:
+ version "0.14.3"
+ resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3"
+ dependencies:
+ is-ci "^1.0.10"
+ normalize-path "^1.0.0"
+ strip-indent "^2.0.0"
+
+iconv-lite@0.4.13:
+ version "0.4.13"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
+
+iconv-lite@^0.4.17:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+
+ieee754@^1.1.4:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
+
+ignore@^3.2.0:
+ version "3.3.5"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.5.tgz#c4e715455f6073a8d7e5dae72d2fc9d71663dba6"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+indent-string@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+ dependencies:
+ repeating "^2.0.0"
+
+indent-string@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
+
+indexof@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+
+ini@^1.3.2, ini@^1.3.4, ini@~1.3.0:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
+
+inquirer@1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-1.2.3.tgz#4dec6f32f37ef7bb0b2ed3f1d1a5c3f545074918"
+ dependencies:
+ ansi-escapes "^1.1.0"
+ chalk "^1.0.0"
+ cli-cursor "^1.0.1"
+ cli-width "^2.0.0"
+ external-editor "^1.1.0"
+ figures "^1.3.5"
+ lodash "^4.3.0"
+ mute-stream "0.0.6"
+ pinkie-promise "^2.0.0"
+ run-async "^2.2.0"
+ rx "^4.1.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.0"
+ through "^2.3.6"
+
+inquirer@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
+ dependencies:
+ ansi-escapes "^1.1.0"
+ ansi-regex "^2.0.0"
+ chalk "^1.0.0"
+ cli-cursor "^1.0.1"
+ cli-width "^2.0.0"
+ figures "^1.3.5"
+ lodash "^4.3.0"
+ readline2 "^1.0.1"
+ run-async "^0.1.0"
+ rx-lite "^3.1.2"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.0"
+ through "^2.3.6"
+
+inquirer@^3.2.2:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
+ dependencies:
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.0"
+ cli-cursor "^2.1.0"
+ cli-width "^2.0.0"
+ external-editor "^2.0.4"
+ figures "^2.0.0"
+ lodash "^4.3.0"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rx-lite "^4.0.8"
+ rx-lite-aggregates "^4.0.8"
+ string-width "^2.1.0"
+ strip-ansi "^4.0.0"
+ through "^2.3.6"
+
+interpret@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0"
+
+invariant@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ dependencies:
+ binary-extensions "^1.0.0"
+
+is-buffer@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-ci@^1.0.10:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e"
+ dependencies:
+ ci-info "^1.0.0"
+
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-glob@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+ dependencies:
+ is-extglob "^2.1.0"
+
+is-glob@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-my-json-valid@^2.10.0:
+ version "2.16.1"
+ resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz#5a846777e2c2620d1e69104e5d3a03b1f6088f11"
+ dependencies:
+ generate-function "^2.0.0"
+ generate-object-property "^1.1.0"
+ jsonpointer "^4.0.0"
+ xtend "^4.0.0"
+
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-obj@^1.0.0, is-obj@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
+
+is-path-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
+
+is-path-in-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc"
+ dependencies:
+ is-path-inside "^1.0.0"
+
+is-path-inside@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f"
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-plain-obj@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+
+is-promise@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+
+is-property@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
+
+is-regexp@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
+
+is-resolvable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.0.0.tgz#8df57c61ea2e3c501408d100fb013cf8d6e0cc62"
+ dependencies:
+ tryit "^1.0.1"
+
+is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+is-subset@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+
+is-text-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e"
+ dependencies:
+ text-extensions "^1.0.0"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+is-utf8@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+
+is-windows@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c"
+
+is-windows@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.1.tgz#310db70f742d259a16a369202b51af84233310d9"
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+istanbul-api@^1.1.1:
+ version "1.1.14"
+ resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.14.tgz#25bc5701f7c680c0ffff913de46e3619a3a6e680"
+ dependencies:
+ async "^2.1.4"
+ fileset "^2.0.2"
+ istanbul-lib-coverage "^1.1.1"
+ istanbul-lib-hook "^1.0.7"
+ istanbul-lib-instrument "^1.8.0"
+ istanbul-lib-report "^1.1.1"
+ istanbul-lib-source-maps "^1.2.1"
+ istanbul-reports "^1.1.2"
+ js-yaml "^3.7.0"
+ mkdirp "^0.5.1"
+ once "^1.4.0"
+
+istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da"
+
+istanbul-lib-hook@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.7.tgz#dd6607f03076578fe7d6f2a630cf143b49bacddc"
+ dependencies:
+ append-transform "^0.4.0"
+
+istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.8.0.tgz#66f6c9421cc9ec4704f76f2db084ba9078a2b532"
+ dependencies:
+ babel-generator "^6.18.0"
+ babel-template "^6.16.0"
+ babel-traverse "^6.18.0"
+ babel-types "^6.18.0"
+ babylon "^6.18.0"
+ istanbul-lib-coverage "^1.1.1"
+ semver "^5.3.0"
+
+istanbul-lib-report@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#f0e55f56655ffa34222080b7a0cd4760e1405fc9"
+ dependencies:
+ istanbul-lib-coverage "^1.1.1"
+ mkdirp "^0.5.1"
+ path-parse "^1.0.5"
+ supports-color "^3.1.2"
+
+istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.1.tgz#a6fe1acba8ce08eebc638e572e294d267008aa0c"
+ dependencies:
+ debug "^2.6.3"
+ istanbul-lib-coverage "^1.1.1"
+ mkdirp "^0.5.1"
+ rimraf "^2.6.1"
+ source-map "^0.5.3"
+
+istanbul-reports@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.2.tgz#0fb2e3f6aa9922bd3ce45d05d8ab4d5e8e07bd4f"
+ dependencies:
+ handlebars "^4.0.3"
+
+jest-changed-files@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-20.0.3.tgz#9394d5cc65c438406149bef1bf4d52b68e03e3f8"
+
+jest-cli@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-20.0.4.tgz#e532b19d88ae5bc6c417e8b0593a6fe954b1dc93"
+ dependencies:
+ ansi-escapes "^1.4.0"
+ callsites "^2.0.0"
+ chalk "^1.1.3"
+ graceful-fs "^4.1.11"
+ is-ci "^1.0.10"
+ istanbul-api "^1.1.1"
+ istanbul-lib-coverage "^1.0.1"
+ istanbul-lib-instrument "^1.4.2"
+ istanbul-lib-source-maps "^1.1.0"
+ jest-changed-files "^20.0.3"
+ jest-config "^20.0.4"
+ jest-docblock "^20.0.3"
+ jest-environment-jsdom "^20.0.3"
+ jest-haste-map "^20.0.4"
+ jest-jasmine2 "^20.0.4"
+ jest-message-util "^20.0.3"
+ jest-regex-util "^20.0.3"
+ jest-resolve-dependencies "^20.0.3"
+ jest-runtime "^20.0.4"
+ jest-snapshot "^20.0.3"
+ jest-util "^20.0.3"
+ micromatch "^2.3.11"
+ node-notifier "^5.0.2"
+ pify "^2.3.0"
+ slash "^1.0.0"
+ string-length "^1.0.1"
+ throat "^3.0.0"
+ which "^1.2.12"
+ worker-farm "^1.3.1"
+ yargs "^7.0.2"
+
+jest-config@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-20.0.4.tgz#e37930ab2217c913605eff13e7bd763ec48faeea"
+ dependencies:
+ chalk "^1.1.3"
+ glob "^7.1.1"
+ jest-environment-jsdom "^20.0.3"
+ jest-environment-node "^20.0.3"
+ jest-jasmine2 "^20.0.4"
+ jest-matcher-utils "^20.0.3"
+ jest-regex-util "^20.0.3"
+ jest-resolve "^20.0.4"
+ jest-validate "^20.0.3"
+ pretty-format "^20.0.3"
+
+jest-diff@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-20.0.3.tgz#81f288fd9e675f0fb23c75f1c2b19445fe586617"
+ dependencies:
+ chalk "^1.1.3"
+ diff "^3.2.0"
+ jest-matcher-utils "^20.0.3"
+ pretty-format "^20.0.3"
+
+jest-docblock@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-20.0.3.tgz#17bea984342cc33d83c50fbe1545ea0efaa44712"
+
+jest-docblock@^21.0.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414"
+
+jest-environment-jsdom@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-20.0.3.tgz#048a8ac12ee225f7190417713834bb999787de99"
+ dependencies:
+ jest-mock "^20.0.3"
+ jest-util "^20.0.3"
+ jsdom "^9.12.0"
+
+jest-environment-node@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-20.0.3.tgz#d488bc4612af2c246e986e8ae7671a099163d403"
+ dependencies:
+ jest-mock "^20.0.3"
+ jest-util "^20.0.3"
+
+jest-get-type@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.2.0.tgz#f6376ab9db4b60d81e39f30749c6c466f40d4a23"
+
+jest-haste-map@^20.0.4:
+ version "20.0.5"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-20.0.5.tgz#abad74efb1a005974a7b6517e11010709cab9112"
+ dependencies:
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.1.11"
+ jest-docblock "^20.0.3"
+ micromatch "^2.3.11"
+ sane "~1.6.0"
+ worker-farm "^1.3.1"
+
+jest-jasmine2@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-20.0.4.tgz#fcc5b1411780d911d042902ef1859e852e60d5e1"
+ dependencies:
+ chalk "^1.1.3"
+ graceful-fs "^4.1.11"
+ jest-diff "^20.0.3"
+ jest-matcher-utils "^20.0.3"
+ jest-matchers "^20.0.3"
+ jest-message-util "^20.0.3"
+ jest-snapshot "^20.0.3"
+ once "^1.4.0"
+ p-map "^1.1.1"
+
+jest-matcher-utils@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-20.0.3.tgz#b3a6b8e37ca577803b0832a98b164f44b7815612"
+ dependencies:
+ chalk "^1.1.3"
+ pretty-format "^20.0.3"
+
+jest-matchers@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-matchers/-/jest-matchers-20.0.3.tgz#ca69db1c32db5a6f707fa5e0401abb55700dfd60"
+ dependencies:
+ jest-diff "^20.0.3"
+ jest-matcher-utils "^20.0.3"
+ jest-message-util "^20.0.3"
+ jest-regex-util "^20.0.3"
+
+jest-message-util@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-20.0.3.tgz#6aec2844306fcb0e6e74d5796c1006d96fdd831c"
+ dependencies:
+ chalk "^1.1.3"
+ micromatch "^2.3.11"
+ slash "^1.0.0"
+
+jest-mock@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-20.0.3.tgz#8bc070e90414aa155c11a8d64c869a0d5c71da59"
+
+jest-regex-util@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-20.0.3.tgz#85bbab5d133e44625b19faf8c6aa5122d085d762"
+
+jest-resolve-dependencies@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-20.0.3.tgz#6e14a7b717af0f2cb3667c549de40af017b1723a"
+ dependencies:
+ jest-regex-util "^20.0.3"
+
+jest-resolve@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-20.0.4.tgz#9448b3e8b6bafc15479444c6499045b7ffe597a5"
+ dependencies:
+ browser-resolve "^1.11.2"
+ is-builtin-module "^1.0.0"
+ resolve "^1.3.2"
+
+jest-runtime@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-20.0.4.tgz#a2c802219c4203f754df1404e490186169d124d8"
+ dependencies:
+ babel-core "^6.0.0"
+ babel-jest "^20.0.3"
+ babel-plugin-istanbul "^4.0.0"
+ chalk "^1.1.3"
+ convert-source-map "^1.4.0"
+ graceful-fs "^4.1.11"
+ jest-config "^20.0.4"
+ jest-haste-map "^20.0.4"
+ jest-regex-util "^20.0.3"
+ jest-resolve "^20.0.4"
+ jest-util "^20.0.3"
+ json-stable-stringify "^1.0.1"
+ micromatch "^2.3.11"
+ strip-bom "3.0.0"
+ yargs "^7.0.2"
+
+jest-snapshot@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-20.0.3.tgz#5b847e1adb1a4d90852a7f9f125086e187c76566"
+ dependencies:
+ chalk "^1.1.3"
+ jest-diff "^20.0.3"
+ jest-matcher-utils "^20.0.3"
+ jest-util "^20.0.3"
+ natural-compare "^1.4.0"
+ pretty-format "^20.0.3"
+
+jest-util@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-20.0.3.tgz#0c07f7d80d82f4e5a67c6f8b9c3fe7f65cfd32ad"
+ dependencies:
+ chalk "^1.1.3"
+ graceful-fs "^4.1.11"
+ jest-message-util "^20.0.3"
+ jest-mock "^20.0.3"
+ jest-validate "^20.0.3"
+ leven "^2.1.0"
+ mkdirp "^0.5.1"
+
+jest-validate@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-20.0.3.tgz#d0cfd1de4f579f298484925c280f8f1d94ec3cab"
+ dependencies:
+ chalk "^1.1.3"
+ jest-matcher-utils "^20.0.3"
+ leven "^2.1.0"
+ pretty-format "^20.0.3"
+
+jest-validate@^21.1.0:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.2.1.tgz#cc0cbca653cd54937ba4f2a111796774530dd3c7"
+ dependencies:
+ chalk "^2.0.1"
+ jest-get-type "^21.2.0"
+ leven "^2.1.0"
+ pretty-format "^21.2.1"
+
+jest@^20.0.4:
+ version "20.0.4"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-20.0.4.tgz#3dd260c2989d6dad678b1e9cc4d91944f6d602ac"
+ dependencies:
+ jest-cli "^20.0.4"
+
+js-tokens@^3.0.0, js-tokens@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
+jschardet@^1.4.2:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.1.tgz#c519f629f86b3a5bedba58a88d311309eec097f9"
+
+jsdom@^9.12.0:
+ version "9.12.0"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4"
+ dependencies:
+ abab "^1.0.3"
+ acorn "^4.0.4"
+ acorn-globals "^3.1.0"
+ array-equal "^1.0.0"
+ content-type-parser "^1.0.1"
+ cssom ">= 0.3.2 < 0.4.0"
+ cssstyle ">= 0.2.37 < 0.3.0"
+ escodegen "^1.6.1"
+ html-encoding-sniffer "^1.0.1"
+ nwmatcher ">= 1.3.9 < 2.0.0"
+ parse5 "^1.5.1"
+ request "^2.79.0"
+ sax "^1.2.1"
+ symbol-tree "^3.2.1"
+ tough-cookie "^2.3.2"
+ webidl-conversions "^4.0.0"
+ whatwg-encoding "^1.0.1"
+ whatwg-url "^4.3.0"
+ xml-name-validator "^2.0.1"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+
+json-schema-traverse@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+json5@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+json@^9.0.6:
+ version "9.0.6"
+ resolved "https://registry.yarnpkg.com/json/-/json-9.0.6.tgz#7972c2a5a48a42678db2730c7c2c4ee6e4e24585"
+
+jsonfile@^2.1.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+jsonfile@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
+jsonparse@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
+
+jsonpointer@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+kind-of@^3.0.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ dependencies:
+ is-buffer "^1.1.5"
+
+klaw@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
+ optionalDependencies:
+ graceful-fs "^4.1.9"
+
+lazy-cache@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ dependencies:
+ invert-kv "^1.0.0"
+
+lerna@^2.0.0-rc.4:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/lerna/-/lerna-2.4.0.tgz#7b76446b154bafb9cba8996f3dc233f1cb6ca7c3"
+ dependencies:
+ async "^1.5.0"
+ chalk "^2.1.0"
+ cmd-shim "^2.0.2"
+ columnify "^1.5.4"
+ command-join "^2.0.0"
+ conventional-changelog-cli "^1.3.2"
+ conventional-recommended-bump "^1.0.1"
+ dedent "^0.7.0"
+ execa "^0.8.0"
+ find-up "^2.1.0"
+ fs-extra "^4.0.1"
+ get-port "^3.2.0"
+ glob "^7.1.2"
+ glob-parent "^3.1.0"
+ globby "^6.1.0"
+ graceful-fs "^4.1.11"
+ hosted-git-info "^2.5.0"
+ inquirer "^3.2.2"
+ is-ci "^1.0.10"
+ load-json-file "^3.0.0"
+ lodash "^4.17.4"
+ minimatch "^3.0.4"
+ npmlog "^4.1.2"
+ p-finally "^1.0.0"
+ path-exists "^3.0.0"
+ read-cmd-shim "^1.0.1"
+ read-pkg "^2.0.0"
+ rimraf "^2.6.1"
+ safe-buffer "^5.1.1"
+ semver "^5.4.1"
+ signal-exit "^3.0.2"
+ strong-log-transformer "^1.0.6"
+ temp-write "^3.3.0"
+ write-file-atomic "^2.3.0"
+ write-json-file "^2.2.0"
+ write-pkg "^3.1.0"
+ yargs "^8.0.2"
+
+leven@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
+
+levn@^0.3.0, levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+lint-staged@^4.0.2:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-4.3.0.tgz#ed0779ad9a42c0dc62bb3244e522870b41125879"
+ dependencies:
+ app-root-path "^2.0.0"
+ chalk "^2.1.0"
+ commander "^2.11.0"
+ cosmiconfig "^1.1.0"
+ execa "^0.8.0"
+ is-glob "^4.0.0"
+ jest-validate "^21.1.0"
+ listr "^0.12.0"
+ lodash "^4.17.4"
+ log-symbols "^2.0.0"
+ minimatch "^3.0.0"
+ npm-which "^3.0.1"
+ p-map "^1.1.1"
+ staged-git-files "0.0.4"
+ stringify-object "^3.2.0"
+
+listr-silent-renderer@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
+
+listr-update-renderer@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz#ca80e1779b4e70266807e8eed1ad6abe398550f9"
+ dependencies:
+ chalk "^1.1.3"
+ cli-truncate "^0.2.1"
+ elegant-spinner "^1.0.1"
+ figures "^1.7.0"
+ indent-string "^3.0.0"
+ log-symbols "^1.0.2"
+ log-update "^1.0.2"
+ strip-ansi "^3.0.1"
+
+listr-verbose-renderer@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.0.tgz#44dc01bb0c34a03c572154d4d08cde9b1dc5620f"
+ dependencies:
+ chalk "^1.1.3"
+ cli-cursor "^1.0.2"
+ date-fns "^1.27.2"
+ figures "^1.7.0"
+
+listr@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/listr/-/listr-0.12.0.tgz#6bce2c0f5603fa49580ea17cd6a00cc0e5fa451a"
+ dependencies:
+ chalk "^1.1.3"
+ cli-truncate "^0.2.1"
+ figures "^1.7.0"
+ indent-string "^2.1.0"
+ is-promise "^2.1.0"
+ is-stream "^1.1.0"
+ listr-silent-renderer "^1.1.1"
+ listr-update-renderer "^0.2.0"
+ listr-verbose-renderer "^0.4.0"
+ log-symbols "^1.0.2"
+ log-update "^1.0.2"
+ ora "^0.2.3"
+ p-map "^1.1.1"
+ rxjs "^5.0.0-beta.11"
+ stream-to-observable "^0.1.0"
+ strip-ansi "^3.0.1"
+
+load-json-file@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+
+load-json-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
+load-json-file@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-3.0.0.tgz#7eb3735d983a7ed2262ade4ff769af5369c5c440"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^3.0.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+lodash._basefor@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2"
+
+lodash._reinterpolate@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
+
+lodash.isarguments@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
+
+lodash.isarray@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
+
+lodash.isplainobject@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-3.2.0.tgz#9a8238ae16b200432960cd7346512d0123fbf4c5"
+ dependencies:
+ lodash._basefor "^3.0.0"
+ lodash.isarguments "^3.0.0"
+ lodash.keysin "^3.0.0"
+
+lodash.keysin@^3.0.0:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/lodash.keysin/-/lodash.keysin-3.0.8.tgz#22c4493ebbedb1427962a54b445b2c8a767fb47f"
+ dependencies:
+ lodash.isarguments "^3.0.0"
+ lodash.isarray "^3.0.0"
+
+lodash.map@^4.5.1:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
+
+lodash.template@^4.0.2:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0"
+ dependencies:
+ lodash._reinterpolate "~3.0.0"
+ lodash.templatesettings "^4.0.0"
+
+lodash.templatesettings@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316"
+ dependencies:
+ lodash._reinterpolate "~3.0.0"
+
+lodash@4.17.2:
+ version "4.17.2"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42"
+
+lodash@^4.0.0, lodash@^4.1.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.1, lodash@^4.3.0:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+
+log-symbols@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
+ dependencies:
+ chalk "^1.0.0"
+
+log-symbols@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.1.0.tgz#f35fa60e278832b538dc4dddcbb478a45d3e3be6"
+ dependencies:
+ chalk "^2.0.1"
+
+log-update@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1"
+ dependencies:
+ ansi-escapes "^1.0.0"
+ cli-cursor "^1.0.2"
+
+lolex@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31"
+
+longest@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
+
+loose-envify@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ dependencies:
+ js-tokens "^3.0.0"
+
+loud-rejection@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+ dependencies:
+ currently-unhandled "^0.4.1"
+ signal-exit "^3.0.0"
+
+lru-cache@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+make-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
+ dependencies:
+ pify "^2.3.0"
+
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ dependencies:
+ tmpl "1.0.x"
+
+map-obj@^1.0.0, map-obj@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+
+md5.js@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d"
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+mem@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+meow@^3.3.0, meow@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+ dependencies:
+ camelcase-keys "^2.0.0"
+ decamelize "^1.1.2"
+ loud-rejection "^1.0.0"
+ map-obj "^1.0.1"
+ minimist "^1.1.3"
+ normalize-package-data "^2.3.4"
+ object-assign "^4.0.1"
+ read-pkg-up "^1.0.1"
+ redent "^1.0.0"
+ trim-newlines "^1.0.0"
+
+merge@^1.1.3, merge@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
+
+micromatch@^2.1.5, micromatch@^2.3.11, micromatch@^2.3.7:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+miller-rabin@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+mime-db@~1.30.0:
+ version "1.30.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
+
+mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7:
+ version "2.1.17"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
+ dependencies:
+ mime-db "~1.30.0"
+
+mimic-fn@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
+
+minimalistic-assert@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+
+minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@1.2.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+minimist@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de"
+
+minimist@~0.0.1:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+
+"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+modify-babel-preset@^3.1.0, modify-babel-preset@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/modify-babel-preset/-/modify-babel-preset-3.2.1.tgz#d7172aa3c0822ed3fc08e308fd0971295136ab50"
+ dependencies:
+ require-relative "^0.8.7"
+
+modify-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.0.tgz#e2b6cdeb9ce19f99317a53722f3dbf5df5eaaab2"
+
+moment@^2.6.0:
+ version "2.19.1"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+mute-stream@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
+
+mute-stream@0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db"
+
+mute-stream@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+
+nan@^2.3.0:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+
+node-libs-browser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646"
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.1.4"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^1.0.0"
+ https-browserify "0.0.1"
+ os-browserify "^0.2.0"
+ path-browserify "0.0.0"
+ process "^0.11.0"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.0.5"
+ stream-browserify "^2.0.1"
+ stream-http "^2.3.1"
+ string_decoder "^0.10.25"
+ timers-browserify "^2.0.2"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.10.3"
+ vm-browserify "0.0.4"
+
+node-notifier@^5.0.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff"
+ dependencies:
+ growly "^1.3.0"
+ semver "^5.3.0"
+ shellwords "^0.1.0"
+ which "^1.2.12"
+
+node-pre-gyp@^0.6.36:
+ version "0.6.38"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.38.tgz#e92a20f83416415bb4086f6d1fb78b3da73d113d"
+ dependencies:
+ hawk "3.1.3"
+ mkdirp "^0.5.1"
+ nopt "^4.0.1"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ request "2.81.0"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^2.2.1"
+ tar-pack "^3.4.0"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379"
+
+normalize-path@^2.0.0, normalize-path@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+npm-path@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.3.tgz#15cff4e1c89a38da77f56f6055b24f975dfb2bbe"
+ dependencies:
+ which "^1.2.10"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ dependencies:
+ path-key "^2.0.0"
+
+npm-which@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/npm-which/-/npm-which-3.0.1.tgz#9225f26ec3a285c209cae67c3b11a6b4ab7140aa"
+ dependencies:
+ commander "^2.9.0"
+ npm-path "^2.0.2"
+ which "^1.2.10"
+
+npmlog@^4.0.2, npmlog@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+"nwmatcher@>= 1.3.9 < 2.0.0":
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.3.tgz#64348e3b3d80f035b40ac11563d278f8b72db89c"
+
+oauth-sign@~0.8.1, oauth-sign@~0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+
+object-assign@^4.0.1, object-assign@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+once@^1.3.0, once@^1.3.3, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+onetime@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
+
+onetime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+optimist@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+ dependencies:
+ minimist "~0.0.1"
+ wordwrap "~0.0.2"
+
+optionator@^0.8.1, optionator@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+ora@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
+ dependencies:
+ chalk "^1.1.1"
+ cli-cursor "^1.0.2"
+ cli-spinners "^0.1.2"
+ object-assign "^4.0.1"
+
+os-browserify@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
+
+os-homedir@^1.0.0, os-homedir@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-locale@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+ dependencies:
+ lcid "^1.0.0"
+
+os-locale@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
+ dependencies:
+ execa "^0.7.0"
+ lcid "^1.0.0"
+ mem "^1.1.0"
+
+os-shim@^0.1.2:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917"
+
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+osenv@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+output-file-sync@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76"
+ dependencies:
+ graceful-fs "^4.1.4"
+ mkdirp "^0.5.1"
+ object-assign "^4.1.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
+p-limit@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ dependencies:
+ p-limit "^1.1.0"
+
+p-map@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
+
+pad-right@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/pad-right/-/pad-right-0.2.2.tgz#6fbc924045d244f2a2a244503060d3bfc6009774"
+ dependencies:
+ repeat-string "^1.5.2"
+
+pako@~0.2.0:
+ version "0.2.9"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
+
+parse-asn1@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
+ dependencies:
+ asn1.js "^4.0.0"
+ browserify-aes "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.0"
+ pbkdf2 "^3.0.3"
+
+parse-github-repo-url@^1.3.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50"
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ dependencies:
+ error-ex "^1.2.0"
+
+parse-json@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-3.0.0.tgz#fa6f47b18e23826ead32f263e744d0e1e847fb13"
+ dependencies:
+ error-ex "^1.3.1"
+
+parse-passwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
+
+parse5@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
+
+path-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
+
+path-dirname@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+
+path-exists@2.1.0, path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ dependencies:
+ pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-is-inside@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+
+path-key@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
+path-type@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+ dependencies:
+ pify "^2.0.0"
+
+pbkdf2@^3.0.3:
+ version "3.0.14"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade"
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+performance-now@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+
+pify@^2.0.0, pify@^2.2.0, pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ dependencies:
+ pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+
+pluralize@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+
+prettier@^1.5.3:
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.7.4.tgz#5e8624ae9363c80f95ec644584ecdf55d74f93fa"
+
+pretty-format@^20.0.3:
+ version "20.0.3"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-20.0.3.tgz#020e350a560a1fe1a98dc3beb6ccffb386de8b14"
+ dependencies:
+ ansi-regex "^2.1.1"
+ ansi-styles "^3.0.0"
+
+pretty-format@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-21.2.1.tgz#ae5407f3cf21066cd011aa1ba5fce7b6a2eddb36"
+ dependencies:
+ ansi-regex "^3.0.0"
+ ansi-styles "^3.2.0"
+
+prettycli@^1.3.0:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/prettycli/-/prettycli-1.4.3.tgz#b28ec2aad9de07ae1fd75ef294fb54cbdee07ed5"
+ dependencies:
+ chalk "2.1.0"
+
+private@^0.1.6, private@^0.1.7:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+
+process-nextick-args@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
+
+process@^0.11.0:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+
+progress@^1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
+
+prr@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
+public-encrypt@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
+ dependencies:
+ bn.js "^4.1.0"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ parse-asn1 "^5.0.0"
+ randombytes "^2.0.1"
+
+punycode@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+
+punycode@^1.2.4, punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+q@^1.4.1:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
+
+qs@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+
+qs@~6.5.1:
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+
+querystring-es3@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+
+querystring@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+
+randomatic@^1.1.3:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+randombytes@^2.0.0, randombytes@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79"
+ dependencies:
+ safe-buffer "^5.1.0"
+
+rc@^1.1.7:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077"
+ dependencies:
+ deep-extend "~0.4.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+read-cmd-shim@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.1.tgz#2d5d157786a37c055d22077c32c53f8329e91c7b"
+ dependencies:
+ graceful-fs "^4.1.2"
+
+read-pkg-up@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+ dependencies:
+ find-up "^1.0.0"
+ read-pkg "^1.0.0"
+
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+ dependencies:
+ find-up "^2.0.0"
+ read-pkg "^2.0.0"
+
+read-pkg@^1.0.0, read-pkg@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+ dependencies:
+ load-json-file "^1.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^1.0.0"
+
+read-pkg@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+ dependencies:
+ load-json-file "^2.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^2.0.0"
+
+readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
+ util-deprecate "~1.0.1"
+
+readdirp@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
+ dependencies:
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ readable-stream "^2.0.2"
+ set-immediate-shim "^1.0.1"
+
+readline2@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ mute-stream "0.0.5"
+
+rechoir@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
+ dependencies:
+ resolve "^1.1.6"
+
+redent@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+ dependencies:
+ indent-string "^2.1.0"
+ strip-indent "^1.0.1"
+
+redux-promise@^0.5.3:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/redux-promise/-/redux-promise-0.5.3.tgz#e97e6c9d3bf376eacb79babe6d906da20112d6d8"
+ dependencies:
+ flux-standard-action "^0.6.1"
+
+redux-thunk@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"
+
+regenerate@^1.2.1:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
+
+regenerator-runtime@^0.10.5:
+ version "0.10.5"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658"
+
+regenerator-runtime@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1"
+
+regenerator-transform@^0.10.0:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
+ dependencies:
+ babel-runtime "^6.18.0"
+ babel-types "^6.19.0"
+ private "^0.1.6"
+
+regex-cache@^0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
+ dependencies:
+ is-equal-shallow "^0.1.3"
+
+regexpu-core@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regjsgen@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
+
+regjsparser@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
+ dependencies:
+ jsesc "~0.5.0"
+
+remove-trailing-separator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+
+repeat-element@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+
+repeat-string@^1.5.2:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ dependencies:
+ is-finite "^1.0.0"
+
+request@2.81.0:
+ version "2.81.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~4.2.1"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ performance-now "^0.2.0"
+ qs "~6.4.0"
+ safe-buffer "^5.0.1"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.0.0"
+
+request@^2.79.0:
+ version "2.83.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.6.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.1"
+ forever-agent "~0.6.1"
+ form-data "~2.3.1"
+ har-validator "~5.0.3"
+ hawk "~6.0.2"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.17"
+ oauth-sign "~0.8.2"
+ performance-now "^2.1.0"
+ qs "~6.5.1"
+ safe-buffer "^5.1.1"
+ stringstream "~0.0.5"
+ tough-cookie "~2.3.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.1.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-from-string@^1.1.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418"
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+require-relative@^0.8.7:
+ version "0.8.7"
+ resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
+
+require-uncached@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
+ dependencies:
+ caller-path "^0.1.0"
+ resolve-from "^1.0.0"
+
+resolve-dir@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e"
+ dependencies:
+ expand-tilde "^1.2.2"
+ global-modules "^0.2.3"
+
+resolve-from@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
+
+resolve@1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+
+resolve@^1.1.6, resolve@^1.3.2:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86"
+ dependencies:
+ path-parse "^1.0.5"
+
+restore-cursor@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
+ dependencies:
+ exit-hook "^1.0.0"
+ onetime "^1.0.0"
+
+restore-cursor@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+ dependencies:
+ onetime "^2.0.0"
+ signal-exit "^3.0.2"
+
+right-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
+ dependencies:
+ align-text "^0.1.1"
+
+right-pad@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0"
+
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
+ dependencies:
+ glob "^7.0.5"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
+ dependencies:
+ hash-base "^2.0.0"
+ inherits "^2.0.1"
+
+rollup-plugin-babel@^2.7.1:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-babel/-/rollup-plugin-babel-2.7.1.tgz#16528197b0f938a1536f44683c7a93d573182f57"
+ dependencies:
+ babel-core "6"
+ babel-plugin-transform-es2015-classes "^6.9.0"
+ object-assign "^4.1.0"
+ rollup-pluginutils "^1.5.0"
+
+rollup-plugin-json@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-json/-/rollup-plugin-json-2.3.0.tgz#3c07a452c1b5391be28006fbfff3644056ce0add"
+ dependencies:
+ rollup-pluginutils "^2.0.1"
+
+rollup-plugin-uglify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-uglify/-/rollup-plugin-uglify-2.0.1.tgz#67b37ad1efdafbd83af4c36b40c189ee4866c969"
+ dependencies:
+ uglify-js "^3.0.9"
+
+rollup-pluginutils@^1.5.0:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz#1e156e778f94b7255bfa1b3d0178be8f5c552408"
+ dependencies:
+ estree-walker "^0.2.1"
+ minimatch "^3.0.2"
+
+rollup-pluginutils@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.0.1.tgz#7ec95b3573f6543a46a6461bd9a7c544525d0fc0"
+ dependencies:
+ estree-walker "^0.3.0"
+ micromatch "^2.3.11"
+
+rollup@^0.41.4:
+ version "0.41.6"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.41.6.tgz#e0d05497877a398c104d816d2733a718a7a94e2a"
+ dependencies:
+ source-map-support "^0.4.0"
+
+run-async@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
+ dependencies:
+ once "^1.3.0"
+
+run-async@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
+ dependencies:
+ is-promise "^2.1.0"
+
+rx-lite-aggregates@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
+ dependencies:
+ rx-lite "*"
+
+rx-lite@*, rx-lite@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
+
+rx-lite@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
+
+rx@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
+
+rxjs@^5.0.0-beta.11:
+ version "5.4.3"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f"
+ dependencies:
+ symbol-observable "^1.0.1"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+samsam@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567"
+
+samsam@~1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621"
+
+sane@~1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-1.6.0.tgz#9610c452307a135d29c1fdfe2547034180c46775"
+ dependencies:
+ anymatch "^1.3.0"
+ exec-sh "^0.2.0"
+ fb-watchman "^1.8.0"
+ minimatch "^3.0.2"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+ watch "~0.10.0"
+
+sax@^1.2.1:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+
+"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@^5.4.1:
+ version "5.4.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+set-immediate-shim@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+
+setimmediate@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+ version "2.4.9"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+shelljs@0.7.6:
+ version "0.7.6"
+ resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.6.tgz#379cccfb56b91c8601e4793356eb5382924de9ad"
+ dependencies:
+ glob "^7.0.0"
+ interpret "^1.0.0"
+ rechoir "^0.6.2"
+
+shelljs@^0.7.5:
+ version "0.7.8"
+ resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
+ dependencies:
+ glob "^7.0.0"
+ interpret "^1.0.0"
+ rechoir "^0.6.2"
+
+shellwords@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+sinon-chai@^2.8.0:
+ version "2.14.0"
+ resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.14.0.tgz#da7dd4cc83cd6a260b67cca0f7a9fdae26a1205d"
+
+sinon@^1.17.7:
+ version "1.17.7"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf"
+ dependencies:
+ formatio "1.1.1"
+ lolex "1.3.2"
+ samsam "1.1.2"
+ util ">=0.10.3 <1"
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+slice-ansi@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
+
+sntp@1.x.x:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
+ dependencies:
+ hoek "2.x.x"
+
+sntp@2.x.x:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b"
+ dependencies:
+ hoek "4.x.x"
+
+sort-keys@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128"
+ dependencies:
+ is-plain-obj "^1.0.0"
+
+source-map-support@^0.4.0, source-map-support@^0.4.15:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
+ dependencies:
+ source-map "^0.5.6"
+
+source-map@^0.4.4:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
+ dependencies:
+ amdefine ">=0.0.4"
+
+source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
+source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
+spawn-sync@^1.0.15:
+ version "1.0.15"
+ resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476"
+ dependencies:
+ concat-stream "^1.4.7"
+ os-shim "^0.1.2"
+
+spdx-correct@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+ dependencies:
+ spdx-license-ids "^1.0.2"
+
+spdx-expression-parse@~1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c"
+
+spdx-license-ids@^1.0.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+
+split2@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493"
+ dependencies:
+ through2 "^2.0.2"
+
+split@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
+ dependencies:
+ through "2"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
+sshpk@^1.7.0:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ dashdash "^1.12.0"
+ getpass "^0.1.1"
+ optionalDependencies:
+ bcrypt-pbkdf "^1.0.0"
+ ecc-jsbn "~0.1.1"
+ jsbn "~0.1.0"
+ tweetnacl "~0.14.0"
+
+staged-git-files@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35"
+
+stream-browserify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-http@^2.3.1:
+ version "2.7.2"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad"
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.2.6"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+stream-to-observable@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe"
+
+string-length@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac"
+ dependencies:
+ strip-ansi "^3.0.0"
+
+string-width@^1.0.1, string-width@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string-width@^2.0.0, string-width@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string_decoder@^0.10.25:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
+string_decoder@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+stringify-object@^3.2.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d"
+ dependencies:
+ get-own-enumerable-property-symbols "^2.0.1"
+ is-obj "^1.0.1"
+ is-regexp "^1.0.0"
+
+stringstream@~0.0.4, stringstream@~0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-bom@3.0.0, strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-bom@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+ dependencies:
+ is-utf8 "^0.2.0"
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
+strip-indent@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+ dependencies:
+ get-stdin "^4.0.1"
+
+strip-indent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
+
+strip-json-comments@2.0.1, strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
+strong-log-transformer@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-1.0.6.tgz#f7fb93758a69a571140181277eea0c2eb1301fa3"
+ dependencies:
+ byline "^5.0.0"
+ duplexer "^0.1.1"
+ minimist "^0.1.0"
+ moment "^2.6.0"
+ through "^2.3.4"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+
+supports-color@^3.1.2:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
+ dependencies:
+ has-flag "^1.0.0"
+
+supports-color@^4.0.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
+ dependencies:
+ has-flag "^2.0.0"
+
+symbol-observable@^1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"
+
+symbol-tree@^3.2.1:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
+
+table@^3.7.8:
+ version "3.8.3"
+ resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
+ dependencies:
+ ajv "^4.7.0"
+ ajv-keywords "^1.0.0"
+ chalk "^1.1.1"
+ lodash "^4.0.0"
+ slice-ansi "0.0.4"
+ string-width "^2.0.0"
+
+tar-pack@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
+ dependencies:
+ debug "^2.2.0"
+ fstream "^1.0.10"
+ fstream-ignore "^1.0.5"
+ once "^1.3.3"
+ readable-stream "^2.1.4"
+ rimraf "^2.5.1"
+ tar "^2.2.1"
+ uid-number "^0.0.6"
+
+tar@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+ dependencies:
+ block-stream "*"
+ fstream "^1.0.2"
+ inherits "2"
+
+temp-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d"
+
+temp-write@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-3.3.0.tgz#c1a96de2b36061342eae81f44ff001aec8f615a9"
+ dependencies:
+ graceful-fs "^4.1.2"
+ is-stream "^1.1.0"
+ make-dir "^1.0.0"
+ pify "^2.2.0"
+ temp-dir "^1.0.0"
+ uuid "^3.0.1"
+
+tempfile@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-1.1.1.tgz#5bcc4eaecc4ab2c707d8bc11d99ccc9a2cb287f2"
+ dependencies:
+ os-tmpdir "^1.0.0"
+ uuid "^2.0.1"
+
+test-exclude@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26"
+ dependencies:
+ arrify "^1.0.1"
+ micromatch "^2.3.11"
+ object-assign "^4.1.0"
+ read-pkg-up "^1.0.1"
+ require-main-filename "^1.0.1"
+
+text-extensions@^1.0.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.7.0.tgz#faaaba2625ed746d568a23e4d0aacd9bf08a8b39"
+
+text-table@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
+throat@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/throat/-/throat-3.2.0.tgz#50cb0670edbc40237b9e347d7e1f88e4620af836"
+
+through2@^2.0.0, through2@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
+ dependencies:
+ readable-stream "^2.1.5"
+ xtend "~4.0.1"
+
+through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
+timers-browserify@^2.0.2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6"
+ dependencies:
+ setimmediate "^1.0.4"
+
+tmp@^0.0.29:
+ version "0.0.29"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0"
+ dependencies:
+ os-tmpdir "~1.0.1"
+
+tmp@^0.0.33:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ dependencies:
+ os-tmpdir "~1.0.2"
+
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+
+to-arraybuffer@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+
+to-fast-properties@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
+tough-cookie@^2.3.2, tough-cookie@~2.3.0, tough-cookie@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
+ dependencies:
+ punycode "^1.4.1"
+
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+
+trim-newlines@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
+
+trim-off-newlines@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+tryit@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
+
+tty-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+type-detect@0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822"
+
+type-detect@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2"
+
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+
+typescript-babel-jest@^1.0.2:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/typescript-babel-jest/-/typescript-babel-jest-1.0.5.tgz#5f5acffb7495cb050601f056e4ec07ac52c69445"
+ dependencies:
+ app-root-path "2.0.1"
+ babel-jest "20.0.3"
+ typescript "^2.4.1"
+
+typescript@^2.2.1, typescript@^2.4.1:
+ version "2.5.3"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d"
+
+uglify-js@^2.6:
+ version "2.8.29"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
+ dependencies:
+ source-map "~0.5.1"
+ yargs "~3.10.0"
+ optionalDependencies:
+ uglify-to-browserify "~1.0.0"
+
+uglify-js@^3.0.9:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.1.4.tgz#8e1efa1244b207588e525c9c1835a33458b90aee"
+ dependencies:
+ commander "~2.11.0"
+ source-map "~0.6.1"
+
+uglify-to-browserify@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
+
+uid-number@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+
+universalify@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
+
+url@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+ dependencies:
+ punycode "1.3.2"
+ querystring "0.2.0"
+
+user-home@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190"
+
+user-home@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
+ dependencies:
+ os-homedir "^1.0.0"
+
+util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+util@0.10.3, "util@>=0.10.3 <1", util@^0.10.3:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ dependencies:
+ inherits "2.0.1"
+
+uuid@^2.0.1:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
+
+uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
+
+v8flags@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"
+ dependencies:
+ user-home "^1.1.1"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
+ dependencies:
+ spdx-correct "~1.0.0"
+ spdx-expression-parse "~1.0.0"
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+vm-browserify@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
+ dependencies:
+ indexof "0.0.1"
+
+walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ dependencies:
+ makeerror "1.0.x"
+
+watch@~0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc"
+
+wcwidth@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
+ dependencies:
+ defaults "^1.0.3"
+
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+
+webidl-conversions@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+
+whatwg-encoding@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz#3c6c451a198ee7aec55b1ec61d0920c67801a5f4"
+ dependencies:
+ iconv-lite "0.4.13"
+
+whatwg-url@^4.3.0:
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0"
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
+which-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+
+which@^1.2.10, which@^1.2.12, which@^1.2.9:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
+ dependencies:
+ string-width "^1.0.2"
+
+window-size@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+
+word-wrap@^1.0.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
+
+wordwrap@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
+
+wordwrap@~0.0.2:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+worker-farm@^1.3.1:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.0.tgz#adfdf0cd40581465ed0a1f648f9735722afd5c8d"
+ dependencies:
+ errno "^0.1.4"
+ xtend "^4.0.1"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write-file-atomic@^2.0.0, write-file-atomic@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ signal-exit "^3.0.2"
+
+write-json-file@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-2.3.0.tgz#2b64c8a33004d54b8698c76d585a77ceb61da32f"
+ dependencies:
+ detect-indent "^5.0.0"
+ graceful-fs "^4.1.2"
+ make-dir "^1.0.0"
+ pify "^3.0.0"
+ sort-keys "^2.0.0"
+ write-file-atomic "^2.0.0"
+
+write-pkg@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/write-pkg/-/write-pkg-3.1.0.tgz#030a9994cc9993d25b4e75a9f1a1923607291ce9"
+ dependencies:
+ sort-keys "^2.0.0"
+ write-json-file "^2.2.0"
+
+write@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
+ dependencies:
+ mkdirp "^0.5.1"
+
+xml-name-validator@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
+
+xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+
+y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+
+yargs-parser@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
+ dependencies:
+ camelcase "^3.0.0"
+
+yargs-parser@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+ dependencies:
+ camelcase "^4.1.0"
+
+yargs@^7.0.2:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
+ dependencies:
+ camelcase "^3.0.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^1.4.0"
+ read-pkg-up "^1.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^1.0.2"
+ which-module "^1.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^5.0.0"
+
+yargs@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
+ dependencies:
+ camelcase "^4.1.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^2.0.0"
+ read-pkg-up "^2.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^7.0.0"
+
+yargs@~3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
+ dependencies:
+ camelcase "^1.0.2"
+ cliui "^2.1.0"
+ decamelize "^1.0.0"
+ window-size "0.1.0"