Skip to content

Commit

Permalink
Merge branch 'master' of github.com:trivago/melody into include-synta…
Browse files Browse the repository at this point in the history
…x-error-message
  • Loading branch information
abertelle committed Jan 16, 2019
2 parents c7ea4b4 + a242ae0 commit 1bcd73e
Show file tree
Hide file tree
Showing 38 changed files with 1,517 additions and 240 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- Throw an error in `melody-redux`'s connect if no store found [#68](https://github.com/trivago/melody/pull/68)
- Introduce `melody-hooks` API [#74](https://github.com/trivago/melody/pull/74)
- `[melody-compiler]` warns on `mount` statement without `as` key [#48](https://github.com/trivago/melody/pull/48)
- Introduce `useAtom` hook [#79](https://github.com/trivago/melody/pulls/79)
- Added async mounting of components [#82](https://github.com/trivago/melody/pull/82)

### Fixes

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@
"url": "git://github.com/trivago/melody.git"
},
"scripts": {
"bootstrap": "npm run lerna bootstrap",
"bootstrap": "lerna bootstrap",
"build": "lerna run build",
"build:release": "cross-env NODE_ENV=release lerna run build",
"clean": "rm -rf packages/*/lib && npm run lerna clean -- --yes",
"clean": "rm -rf packages/*/lib && lerna clean -- --yes",
"coveralls": "cat ./coverage/lcov.info | coveralls",
"prebundlesize": "cross-env NODE_ENV=production npm run lerna run build",
"prebundlesize": "cross-env NODE_ENV=production lerna run build",
"bundlesize": "bundlesize",
"precommit": "lint-staged",
"cz": "git-cz",
"fix-lint": "eslint --fix ./packages/*/src",
"flow": "flow; test $? -eq 0 -o $? -eq 2",
"lerna": "lerna",
"lint": "eslint ./packages/*/src",
"prepare": "npm run bootstrap && npm run build:release",
"prepare": "yarn bootstrap && yarn build:release",
"pretest": "./bin/pretest.sh",
"posttest": "./bin/posttest.sh",
"prettier": "prettier --write \"./packages/melody-*/src/**/*.[tj]s\"",
Expand Down
10 changes: 3 additions & 7 deletions packages/melody-code-frame/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,9 @@ export default function({ rawLines, lineNumber, colNumber, length }) {
}

if (colNumber) {
params.line += `\n${params.before}${repeat(
' ',
params.width
)}${params.after}${repeat(' ', colNumber)}${repeat(
'^',
length
)}`;
params.line += `\n${params.before}${repeat(' ', params.width)}${
params.after
}${repeat(' ', colNumber)}${repeat('^', length)}`;
}

params.before = params.before.replace(/^./, '>');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% mount async './parts/#{ part }.twig' as 'bar' with {foo: 'bar'} delay placeholder by 1s %}
{% endmount %}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,36 @@
{% mount 'foo.twig' as 'bar' with {foo: 'bar'} %}

{% mount Foo as 'bar' %}
{% mount Foo as 'bar' with {foo: 'bar'} %}
{% mount async as 'bar' with {foo: 'bar'} %}

{% mount async 'foo.twig' as 'bar' with {foo: 'bar'} %}
Loading... {{ err }}
{% catch err %}
Failed to load with {{ err }}
{% endmount %}

{% mount async 'foo.twig' as 'bar' with {foo: 'bar'} delay placeholder by 500ms %}
Loading... {{ err }}
{% catch err %}
Failed to load with {{ err }}
{% endmount %}

{% mount async 'foo.twig' as 'bar' with {foo: 'bar'} delay placeholder by 1s %}
Loading... {{ err }}
{% catch error %}
Failed to load with {{ error }} {{err}}
{% endmount %}

{% mount async './parts/#{ part }.twig' as 'bar-#{part}' with {foo: 'bar'} delay placeholder by 1s %}
Loading... {{ err }}
{% catch err %}
Failed to load with {{ err }}
{% endmount %}

{% mount async './parts/#{ part }.twig' as 'bar' with {foo: 'bar'} delay placeholder by 1s %}
Loading... {{ err }}
{% endmount %}

{% mount async './parts/#{ part }.twig' as 'bar' with {foo: 'bar'} delay placeholder by 1s %}
<strong>Loading...</strong>
{% endmount %}
102 changes: 100 additions & 2 deletions packages/melody-compiler/__tests__/__snapshots__/CompilerSpec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1588,8 +1588,9 @@ export default function Macros(props) {

exports[`Compiler should correctly transform mount.template 1`] = `
"
import { AsyncComponent } from \\"melody-runtime\\";
import Footwig from \\"foo.twig\\";
import { component } from \\"melody-idom\\";
import { component, text, elementOpen, elementClose } from \\"melody-idom\\";
import Component from \\"./component\\";
export const _template = {};
Expand All @@ -1600,9 +1601,93 @@ _template.render = function (_context) {
foo: \\"bar\\"
});
component(_context.Foo, \\"bar\\");
component(_context.Foo, \\"bar\\", {
component(_context.async, \\"bar\\", {
foo: \\"bar\\"
});
component(AsyncComponent, \\"bar\\", {
promisedComponent: () => import( /* webpackChunkName: \\"bar\\", webpackPrefetch: true */\\"foo.twig\\"),
delayLoadingAnimation: 0,
whileLoading: () => {
text(\\" Loading... \\");
text(_context.err);
},
onError: err => {
text(\\" Failed to load with \\");
text(err);
},
data: {
foo: \\"bar\\"
}
});
component(AsyncComponent, \\"bar\\", {
promisedComponent: () => import( /* webpackChunkName: \\"bar\\", webpackPrefetch: true */\\"foo.twig\\"),
delayLoadingAnimation: 500,
whileLoading: () => {
text(\\" Loading... \\");
text(_context.err);
},
onError: err => {
text(\\" Failed to load with \\");
text(err);
},
data: {
foo: \\"bar\\"
}
});
component(AsyncComponent, \\"bar\\", {
promisedComponent: () => import( /* webpackChunkName: \\"bar\\", webpackPrefetch: true */\\"foo.twig\\"),
delayLoadingAnimation: 1000,
whileLoading: () => {
text(\\" Loading... \\");
text(_context.err);
},
onError: error => {
text(\\" Failed to load with \\");
text(error);
text(_context.err);
},
data: {
foo: \\"bar\\"
}
});
component(AsyncComponent, \\"\\" + (\\"bar-\\" + _context.part), {
promisedComponent: () => import( /* webpackPrefetch: true */\\"./parts/\\" + _context.part + \\".twig\\"),
delayLoadingAnimation: 1000,
whileLoading: () => {
text(\\" Loading... \\");
text(_context.err);
},
onError: err => {
text(\\" Failed to load with \\");
text(err);
},
data: {
foo: \\"bar\\"
}
});
component(AsyncComponent, \\"bar\\", {
promisedComponent: () => import( /* webpackChunkName: \\"bar\\", webpackPrefetch: true */\\"./parts/\\" + _context.part + \\".twig\\"),
delayLoadingAnimation: 1000,
whileLoading: () => {
text(\\" Loading... \\");
text(_context.err);
},
data: {
foo: \\"bar\\"
}
});
component(AsyncComponent, \\"bar\\", {
promisedComponent: () => import( /* webpackChunkName: \\"bar\\", webpackPrefetch: true */\\"./parts/\\" + _context.part + \\".twig\\"),
delayLoadingAnimation: 1000,
whileLoading: () => {
elementOpen(\\"strong\\", null, null);
text(\\"Loading...\\");
elementClose(\\"strong\\");
},
data: {
foo: \\"bar\\"
}
});
};
if (process.env.NODE_ENV !== \\"production\\") {
Expand Down Expand Up @@ -2179,6 +2264,19 @@ export default function Svg(props) {
"
`;

exports[`Compiler should fail transforming empty async mount.template 1`] = `
"Asynchronously mounted components must have a placeholder
> 1 | {% mount async './parts/#{ part }.twig' as 'bar' with {foo: 'bar'} delay placeholder by 1s %}
| ^^^^^^^^^^^
2 | {% endmount %}
3 |
When using an async component you must provide a placeholder that can be rendered while your component is being loaded.
Example:
{% mount async 'my-component' as 'mycomp' %}
This is the placeholder content that will be shown to your users while the async component is being loaded."
`;

exports[`Compiler should fail transforming unknown filter.template 1`] = `
"Unknown filter \\"unknown\\"
> 1 | {{ test | unknown }}
Expand Down
12 changes: 7 additions & 5 deletions packages/melody-extension-core/src/parser/autoescape.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ export const AutoescapeParser = {
'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)}.`,
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)) {
Expand All @@ -45,9 +47,9 @@ I expected the current string to end with a ${stringStartToken.text} but instead
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.`,
advice: `Expected type of autoescape to be a string, boolean or not specified. Found ${
tokens.la(0).type
} instead.`,
});
}

Expand Down
8 changes: 5 additions & 3 deletions packages/melody-extension-core/src/parser/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ export const BlockParser = {
pos: unexpectedToken.pos,
advice:
unexpectedToken.type == Types.SYMBOL
? `Expected end of block ${nameToken.text} but instead found end of block ${tokens.la(
0
).text}.`
? `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.`,
Expand Down
3 changes: 2 additions & 1 deletion packages/melody-extension-core/src/parser/if.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export const IfParser = {
test = parser.matchExpression();
tokens.expect(Types.TAG_END);
const consequent = parser.parse(matchConsequent).expressions;
alternate = (alternate || ifStatement
alternate = (
alternate || ifStatement
).alternate = new IfStatement(test, consequent);
}

Expand Down
4 changes: 3 additions & 1 deletion packages/melody-extension-core/src/parser/macro.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export const MacroParser = {
var nameEndToken = tokens.next();
if (nameToken.text !== nameEndToken.text) {
parser.error({
title: `Macro name mismatch, expected "${nameToken.text}" but found "${nameEndToken.text}"`,
title: `Macro name mismatch, expected "${
nameToken.text
}" but found "${nameEndToken.text}"`,
pos: nameEndToken.pos,
});
}
Expand Down
64 changes: 63 additions & 1 deletion packages/melody-extension-core/src/parser/mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,19 @@ export const MountParser = {
let name = null,
source = null,
key = null,
async = false,
delayBy = 0,
argument = null;

if (tokens.test(Types.SYMBOL, 'async')) {
// we might be looking at an async mount
const nextToken = tokens.la(1);
if (nextToken.type === Types.STRING_START) {
async = true;
tokens.next();
}
}

if (tokens.test(Types.STRING_START)) {
source = parser.matchStringExpression();
} else {
Expand All @@ -50,7 +61,58 @@ export const MountParser = {
argument = parser.matchExpression();
}

const mountStatement = new MountStatement(name, source, key, argument);
if (async) {
if (tokens.nextIf(Types.SYMBOL, 'delay')) {
tokens.expect(Types.SYMBOL, 'placeholder');
tokens.expect(Types.SYMBOL, 'by');
delayBy = Number.parseInt(tokens.expect(Types.NUMBER).text, 10);
if (tokens.nextIf(Types.SYMBOL, 's')) {
delayBy *= 1000;
} else {
tokens.expect(Types.SYMBOL, 'ms');
}
}
}

const mountStatement = new MountStatement(
name,
source,
key,
argument,
async,
delayBy
);

if (async) {
tokens.expect(Types.TAG_END);

mountStatement.body = parser.parse((tokenText, token, tokens) => {
return (
token.type === Types.TAG_START &&
(tokens.test(Types.SYMBOL, 'catch') ||
tokens.test(Types.SYMBOL, 'endmount'))
);
});

if (tokens.nextIf(Types.SYMBOL, 'catch')) {
const errorVariableName = tokens.expect(Types.SYMBOL);
mountStatement.errorVariableName = createNode(
Identifier,
errorVariableName,
errorVariableName.text
);
tokens.expect(Types.TAG_END);
mountStatement.otherwise = parser.parse(
(tokenText, token, tokens) => {
return (
token.type === Types.TAG_START &&
tokens.test(Types.SYMBOL, 'endmount')
);
}
);
}
tokens.expect(Types.SYMBOL, 'endmount');
}

setStartFromToken(mountStatement, token);
setEndFromToken(mountStatement, tokens.expect(Types.TAG_END));
Expand Down
21 changes: 18 additions & 3 deletions packages/melody-extension-core/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,33 @@ export class MountStatement extends Node {
name?: Identifier,
source?: String,
key?: Node,
argument?: Node
argument?: Node,
async?: Boolean,
delayBy?: Number
) {
super();
this.name = name;
this.source = source;
this.key = key;
this.argument = argument;
this.async = async;
this.delayBy = delayBy;
this.errorVariableName = null;
this.body = null;
this.otherwise = null;
}
}
type(MountStatement, 'MountStatement');
alias(MountStatement, 'Statement');
visitor(MountStatement, 'name', 'source', 'key', 'argument');
alias(MountStatement, 'Statement', 'Scope');
visitor(
MountStatement,
'name',
'source',
'key',
'argument',
'body',
'otherwise'
);

export class DoStatement extends Node {
constructor(expression: Node) {
Expand Down

0 comments on commit 1bcd73e

Please sign in to comment.