vdom support

This commit is contained in:
Patrick Steele-Idem 2016-09-13 13:55:49 -06:00
parent f309250843
commit e76c7fa6d6
50 changed files with 1423 additions and 497 deletions

View File

@ -1,4 +1,4 @@
Copyright 2011 eBay Software Foundation
Copyright 2016 eBay Software Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -61,6 +61,10 @@ function makeNode(arg) {
return arg;
} else if (arg == null) {
return undefined;
} else if (Array.isArray(arg)) {
return arg.map((arg) => {
return makeNode(arg);
});
} else {
throw new Error('Argument should be a string or Node or null. Actual: ' + arg);
}

View File

@ -39,9 +39,11 @@ class FinalNodes {
return;
}
if (node instanceof Html && this.lastNode instanceof Html) {
this.lastNode.append(node);
return;
if (node instanceof Html) {
if (this.lastNode instanceof Html) {
this.lastNode.append(node);
return;
}
}
if (node.setFinalNode) {
@ -185,6 +187,7 @@ class CodeGenerator {
node.setCodeGenerator(null);
generatedCode = this._invokeCodeGenerator(codeGeneratorFunc, node, false);
if (generatedCode != null && generatedCode !== node) {
node = null;
this._generateCode(generatedCode, finalNodes);

View File

@ -9,8 +9,10 @@ const Container = require('./ast/Container');
const isValidJavaScriptVarName = require('./util/isValidJavaScriptVarName');
class CodeWriter {
constructor(options) {
constructor(options, builder) {
ok(builder, '"builder" is required');
options = options || {};
this.builder = builder;
this.root = null;
this._indentStr = options.indent != null ? options.indent : ' ';
this._indentSize = this._indentStr.length;

View File

@ -13,6 +13,8 @@ var Node = require('./ast/Node');
var macros = require('./util/macros');
var extend = require('raptor-util/extend');
var Walker = require('./Walker');
var EventEmitter = require('events').EventEmitter;
var utilFingerprint = require('./util/fingerprint');
const FLAG_PRESERVE_WHITESPACE = 'PRESERVE_WHITESPACE';
@ -66,8 +68,9 @@ const helpers = {
'loadTemplate': 'l'
};
class CompileContext {
class CompileContext extends EventEmitter {
constructor(src, filename, builder, options) {
super();
ok(typeof src === 'string', '"src" string is required');
ok(filename, '"filename" is required');
@ -103,6 +106,8 @@ class CompileContext {
}
this._helpers = {};
this._imports = {};
this._fingerprint = undefined;
}
setInline(isInline) {
@ -184,9 +189,16 @@ class CompileContext {
throw new Error('"path" should be a string');
}
return this.addStaticVar(varName, 'require("' + path + '")');
}
var varId = this._imports[path];
if (!varId) {
var builder = this.builder;
var requireFuncCall = this.builder.require(builder.literal(path));
this._imports[path] = varId = this.addStaticVar(varName, requireFuncCall);
}
return varId;
}
addVar(name, init) {
var actualVarName = this._uniqueVars.addVar(name, init);
@ -527,6 +539,19 @@ class CompileContext {
return helperIdentifier;
}
getFingerprint(len) {
var fingerprint = this._fingerprint;
if (!fingerprint) {
this._fingerprint = fingerprint = utilFingerprint(this.src);
}
if (len == null || len >= this._fingerprint) {
return fingerprint;
} else {
return fingerprint.substring(0, len);
}
}
}
CompileContext.prototype.util = {

View File

@ -87,7 +87,7 @@ class CompiledTemplate {
handleErrors(this.context);
// console.log(module.id, 'FINAL AST:' + JSON.stringify(finalAST, null, 4));
var codeWriter = new CodeWriter(this.context.options);
var codeWriter = new CodeWriter(this.context.options, this.context.builder);
codeWriter.write(this.ast);
handleErrors(this.context);

View File

@ -58,6 +58,7 @@ class InlineCompiler {
constructor(context, compiler) {
this.context = context;
this.compiler = compiler;
this.builder = context.builder;
context.setInline(true);
}
@ -75,7 +76,7 @@ class InlineCompiler {
return null;
}
let codeWriter = new CodeWriter(this.context.options);
let codeWriter = new CodeWriter(this.context.options, this.builder);
codeWriter.write(staticNodes);
return codeWriter.getCode();
}

View File

@ -114,7 +114,7 @@ class Parser {
var builder = this.context.builder;
if (this.prevTextNode && this.prevTextNode.isLiteral()) {
this.prevTextNode.appendText(text);
this.prevTextNode.argument.value += text;
} else {
var escape = false;
this.prevTextNode = builder.text(builder.literal(text), escape);

View File

@ -8,6 +8,8 @@ class Walker {
constructor(options) {
this._enter = options.enter || noop;
this._exit = options.exit || noop;
this._enterArray = options.enterArray || noop;
this._exitArray = options.exitArray || noop;
this._stopped = false;
this._reset();
this._stack = [];
@ -38,6 +40,8 @@ class Walker {
_walkArray(array) {
var hasRemoval = false;
array = this._enterArray(array) || array;
array.forEach((node, i) => {
var transformed = this.walk(node);
if (transformed == null) {
@ -56,6 +60,8 @@ class Walker {
}
}
array = this._exitArray(array) || array;
return array;
}

View File

@ -43,7 +43,7 @@ class Html extends Node {
}
}
generateCode() {
generateHTMLCode() {
return this;
}

View File

@ -1,10 +1,7 @@
'use strict';
var Node = require('./Node');
var Literal = require('./Literal');
var ok = require('assert').ok;
var escapeXmlAttr = require('raptor-util/escapeXml').attr;
var attr = require('raptor-util/attr');
var compiler = require('../');
var escapeXmlAttr = require('raptor-util/escapeXml').attr;
function isStringLiteral(node) {
return node.type === 'Literal' && typeof node.value === 'string';
@ -112,98 +109,29 @@ function generateCodeForExpressionAttr(name, value, escape, codegen) {
return finalNodes;
}
module.exports = function generateCode(node, codegen) {
let name = node.name;
let value = node.value;
let argument = node.argument;
let escape = node.escape !== false;
var builder = codegen.builder;
function beforeGenerateCode(event) {
event.codegen.isInAttribute = true;
}
function afterGenerateCode(event) {
event.codegen.isInAttribute = false;
}
class HtmlAttribute extends Node {
constructor(def) {
super('HtmlAttribute');
ok(def, 'Invalid attribute definition');
this.type = 'HtmlAttribute';
this.name = def.name;
this.value = def.value;
this.rawValue = def.rawValue;
this.escape = def.escape;
if (typeof this.value === 'string') {
this.value = compiler.builder.parseExpression(this.value);
}
if (this.value && !(this.value instanceof Node)) {
throw new Error('"value" should be a Node instance');
}
this.argument = def.argument;
this.def = def.def; // The attribute definition loaded from the taglib (if any)
this.on('beforeGenerateCode', beforeGenerateCode);
this.on('afterGenerateCode', afterGenerateCode);
if (!name) {
return null;
}
isLiteralValue() {
return this.value instanceof Literal;
if (node.isLiteralValue()) {
return builder.htmlLiteral(attr(name, value.value));
} else if (value != null) {
return generateCodeForExpressionAttr(name, value, escape, codegen);
} else if (argument) {
return [
builder.htmlLiteral(' ' + name + '('),
builder.htmlLiteral(argument),
builder.htmlLiteral(')')
];
} else {
// Attribute with no value is a boolean attribute
return builder.htmlLiteral(' ' + name);
}
isLiteralString() {
return this.isLiteralValue() &&
typeof this.value.value === 'string';
}
isLiteralBoolean() {
return this.isLiteralValue() &&
typeof this.value.value === 'boolean';
}
generateHTMLCode(codegen) {
let name = this.name;
let value = this.value;
let argument = this.argument;
let escape = this.escape !== false;
var builder = codegen.builder;
if (!name) {
return null;
}
if (this.isLiteralValue()) {
return builder.htmlLiteral(attr(name, value.value));
} else if (value != null) {
return generateCodeForExpressionAttr(name, value, escape, codegen);
} else if (argument) {
return [
builder.htmlLiteral(' ' + name + '('),
builder.htmlLiteral(argument),
builder.htmlLiteral(')')
];
} else {
// Attribute with no value is a boolean attribute
return builder.htmlLiteral(' ' + name);
}
}
walk(walker) {
this.value = walker.walk(this.value);
}
get literalValue() {
if (this.isLiteralValue()) {
return this.value.value;
} else {
throw new Error('Attribute value is not a literal value. Actual: ' + JSON.stringify(this.value, null, 2));
}
}
}
HtmlAttribute.isHtmlAttribute = function(attr) {
return (attr instanceof HtmlAttribute);
};
module.exports = HtmlAttribute;
};

View File

@ -0,0 +1,85 @@
'use strict';
var Node = require('../Node');
var Literal = require('../Literal');
var ok = require('assert').ok;
var compiler = require('../../');
var generateHTMLCode = require('./html/generateCode');
var generateVDOMCode = require('./vdom/generateCode');
var vdomUtil = require('../../util/vdom');
function beforeGenerateCode(event) {
event.codegen.isInAttribute = true;
}
function afterGenerateCode(event) {
event.codegen.isInAttribute = false;
}
class HtmlAttribute extends Node {
constructor(def) {
super('HtmlAttribute');
ok(def, 'Invalid attribute definition');
this.type = 'HtmlAttribute';
this.name = def.name;
this.value = def.value;
this.rawValue = def.rawValue;
this.escape = def.escape;
if (typeof this.value === 'string') {
this.value = compiler.builder.parseExpression(this.value);
}
if (this.value && !(this.value instanceof Node)) {
throw new Error('"value" should be a Node instance');
}
this.argument = def.argument;
this.def = def.def; // The attribute definition loaded from the taglib (if any)
this.on('beforeGenerateCode', beforeGenerateCode);
this.on('afterGenerateCode', afterGenerateCode);
}
generateHTMLCode(codegen) {
return generateHTMLCode(this, codegen);
}
generateVDOMCode(codegen) {
return generateVDOMCode(this, codegen, vdomUtil);
}
isLiteralValue() {
return this.value instanceof Literal;
}
isLiteralString() {
return this.isLiteralValue() &&
typeof this.value.value === 'string';
}
isLiteralBoolean() {
return this.isLiteralValue() &&
typeof this.value.value === 'boolean';
}
walk(walker) {
this.value = walker.walk(this.value);
}
get literalValue() {
if (this.isLiteralValue()) {
return this.value.value;
} else {
throw new Error('Attribute value is not a literal value. Actual: ' + JSON.stringify(this.value, null, 2));
}
}
}
HtmlAttribute.isHtmlAttribute = function(attr) {
return (attr instanceof HtmlAttribute);
};
module.exports = HtmlAttribute;

View File

@ -0,0 +1,6 @@
module.exports = function generateCode(node, codegen, vdomUtil) {
node.name = codegen.generateCode(node.name);
node.value = codegen.generateCode(node.value);
node.isStatic = vdomUtil.isStaticValue(node.value);
return node;
};

View File

@ -1,295 +0,0 @@
'use strict';
var Node = require('./Node');
var Literal = require('./Literal');
var HtmlAttributeCollection = require('./HtmlAttributeCollection');
class StartTag extends Node {
constructor(def) {
super('StartTag');
this.tagName = def.tagName;
this.attributes = def.attributes;
this.argument = def.argument;
this.selfClosed = def.selfClosed;
this.dynamicAttributes = def.dynamicAttributes;
}
generateCode(codegen) {
var builder = codegen.builder;
var tagName = this.tagName;
var selfClosed = this.selfClosed;
var dynamicAttributes = this.dynamicAttributes;
var context = codegen.context;
var nodes = [
builder.htmlLiteral('<'),
builder.html(tagName),
];
var attributes = this.attributes;
if (attributes) {
for (let i=0; i<attributes.length; i++) {
let attr = attributes[i];
nodes.push(codegen.generateCode(attr));
}
}
if (dynamicAttributes) {
dynamicAttributes.forEach(function(attrsExpression) {
let attrsFunctionCall = builder.functionCall(context.helper('attrs'), [attrsExpression]);
nodes.push(builder.html(attrsFunctionCall));
});
}
if (selfClosed) {
nodes.push(builder.htmlLiteral('/>'));
} else {
nodes.push(builder.htmlLiteral('>'));
}
return nodes;
}
}
class EndTag extends Node {
constructor(def) {
super('EndTag');
this.tagName = def.tagName;
}
generateCode(codegen) {
var tagName = this.tagName;
var builder = codegen.builder;
return [
builder.htmlLiteral('</'),
builder.html(tagName),
builder.htmlLiteral('>')
];
}
}
function beforeGenerateCode(event) {
if (event.node.tagName === 'script') {
event.context.pushFlag('SCRIPT_BODY');
}
}
function afterGenerateCode(event) {
if (event.node.tagName === 'script') {
event.context.popFlag('SCRIPT_BODY');
}
}
class HtmlElement extends Node {
constructor(def) {
super('HtmlElement');
this.tagName = null;
this.tagNameExpression = null;
this.setTagName(def.tagName);
this._attributes = def.attributes;
this.body = this.makeContainer(def.body);
this.argument = def.argument;
if (!(this._attributes instanceof HtmlAttributeCollection)) {
this._attributes = new HtmlAttributeCollection(this._attributes);
}
this.openTagOnly = def.openTagOnly;
this.selfClosed = def.selfClosed;
this.dynamicAttributes = undefined;
this.bodyOnlyIf = undefined;
this.on('beforeGenerateCode', beforeGenerateCode);
this.on('afterGenerateCode', afterGenerateCode);
}
generateHTMLCode(codegen) {
var tagName = this.tagName;
// Convert the tag name into a Node so that we generate the code correctly
if (tagName) {
tagName = codegen.builder.literal(tagName);
} else {
tagName = this.tagNameExpression;
}
var context = codegen.context;
if (context.isMacro(this.tagName)) {
// At code generation time, if this tag corresponds to a registered macro
// then invoke the macro based on this HTML element instead of generating
// the code to render an HTML element.
return codegen.builder.invokeMacroFromEl(this);
}
var attributes = this._attributes && this._attributes.all;
var body = this.body;
var argument = this.argument;
var hasBody = body && body.length;
var openTagOnly = this.openTagOnly;
var bodyOnlyIf = this.bodyOnlyIf;
var dynamicAttributes = this.dynamicAttributes;
var selfClosed = this.selfClosed === true;
var builder = codegen.builder;
if (hasBody) {
body = codegen.generateCode(body);
}
if (hasBody || bodyOnlyIf) {
openTagOnly = false;
selfClosed = false;
} else if (selfClosed){
openTagOnly = true;
}
var startTag = new StartTag({
tagName: tagName,
attributes: attributes,
argument: argument,
selfClosed: selfClosed,
dynamicAttributes: dynamicAttributes
});
var endTag;
if (!openTagOnly) {
endTag = new EndTag({
tagName: tagName
});
}
if (bodyOnlyIf) {
var startIf = builder.ifStatement(builder.negate(bodyOnlyIf), [
startTag
]);
var endIf = builder.ifStatement(builder.negate(bodyOnlyIf), [
endTag
]);
return [
startIf,
body,
endIf
];
} else {
if (openTagOnly) {
return codegen.generateCode(startTag);
} else {
return [
startTag,
body,
endTag
];
}
}
}
addDynamicAttributes(expression) {
if (!this.dynamicAttributes) {
this.dynamicAttributes = [];
}
this.dynamicAttributes.push(expression);
}
getAttribute(name) {
return this._attributes != null && this._attributes.getAttribute(name);
}
getAttributeValue(name) {
var attr = this._attributes != null && this._attributes.getAttribute(name);
if (attr) {
return attr.value;
}
}
addAttribute(attr) {
this._attributes.addAttribute(attr);
}
setAttributeValue(name, value) {
this._attributes.setAttributeValue(name, value);
}
replaceAttributes(newAttributes) {
this._attributes.replaceAttributes(newAttributes);
}
removeAttribute(name) {
if (this._attributes) {
this._attributes.removeAttribute(name);
}
}
removeAllAttributes() {
this._attributes.removeAllAttributes();
}
hasAttribute(name) {
return this._attributes != null && this._attributes.hasAttribute(name);
}
getAttributes() {
return this._attributes.all;
}
get attributes() {
return this._attributes.all;
}
forEachAttribute(callback, thisObj) {
var attributes = this._attributes.all.concat([]);
for (let i=0, len=attributes.length; i<len; i++) {
callback.call(thisObj, attributes[i]);
}
}
setTagName(tagName) {
this.tagName = null;
this.tagNameExpression = null;
if (tagName instanceof Node) {
if (tagName instanceof Literal) {
this.tagName = tagName.value;
this.tagNameExpression = tagName;
} else {
this.tagNameExpression = tagName;
}
} else if (typeof tagName === 'string') {
this.tagNameExpression = new Literal({value: tagName});
this.tagName = tagName;
}
}
toJSON() {
return {
type: this.type,
tagName: this.tagName,
attributes: this._attributes,
argument: this.argument,
body: this.body,
bodyOnlyIf: this.bodyOnlyIf,
dynamicAttributes: this.dynamicAttributes
};
}
setBodyOnlyIf(condition) {
this.bodyOnlyIf = condition;
}
walk(walker) {
this.setTagName(walker.walk(this.tagNameExpression));
this._attributes.walk(walker);
this.body = walker.walk(this.body);
}
}
module.exports = HtmlElement;

View File

@ -0,0 +1,23 @@
'use strict';
var Node = require('../../Node');
class EndTag extends Node {
constructor(def) {
super('EndTag');
this.tagName = def.tagName;
}
generateCode(codegen) {
var tagName = this.tagName;
var builder = codegen.builder;
return [
builder.htmlLiteral('</'),
builder.html(tagName),
builder.htmlLiteral('>')
];
}
}
module.exports = EndTag;

View File

@ -0,0 +1,55 @@
'use strict';
var Node = require('../../Node');
class StartTag extends Node {
constructor(def) {
super('StartTag');
this.tagName = def.tagName;
this.attributes = def.attributes;
this.argument = def.argument;
this.selfClosed = def.selfClosed;
this.dynamicAttributes = def.dynamicAttributes;
}
generateCode(codegen) {
var builder = codegen.builder;
var tagName = this.tagName;
var selfClosed = this.selfClosed;
var dynamicAttributes = this.dynamicAttributes;
var context = codegen.context;
var nodes = [
builder.htmlLiteral('<'),
builder.html(tagName),
];
var attributes = this.attributes;
if (attributes) {
for (let i=0; i<attributes.length; i++) {
let attr = attributes[i];
nodes.push(codegen.generateCode(attr));
}
}
if (dynamicAttributes) {
dynamicAttributes.forEach(function(attrsExpression) {
let attrsFunctionCall = builder.functionCall(context.helper('attrs'), [attrsExpression]);
nodes.push(builder.html(attrsFunctionCall));
});
}
if (selfClosed) {
nodes.push(builder.htmlLiteral('/>'));
} else {
nodes.push(builder.htmlLiteral('>'));
}
return nodes;
}
}
module.exports = StartTag;

View File

@ -0,0 +1,88 @@
'use strict';
var StartTag = require('./StartTag');
var EndTag = require('./EndTag');
module.exports = function generateCode(node, codegen) {
var tagName = node.tagName;
// Convert the tag name into a Node so that we generate the code correctly
if (tagName) {
tagName = codegen.builder.literal(tagName);
} else {
tagName = node.tagNameExpression;
}
var context = codegen.context;
if (context.isMacro(node.tagName)) {
// At code generation time, if node tag corresponds to a registered macro
// then invoke the macro based on node HTML element instead of generating
// the code to render an HTML element.
return codegen.builder.invokeMacroFromEl(node);
}
var attributes = node._attributes && node._attributes.all;
var body = node.body;
var argument = node.argument;
var hasBody = body && body.length;
var openTagOnly = node.openTagOnly;
var bodyOnlyIf = node.bodyOnlyIf;
var dynamicAttributes = node.dynamicAttributes;
var selfClosed = node.selfClosed === true;
var builder = codegen.builder;
if (hasBody) {
body = codegen.generateCode(body);
}
if (hasBody || bodyOnlyIf) {
openTagOnly = false;
selfClosed = false;
} else if (selfClosed){
openTagOnly = true;
}
var startTag = new StartTag({
tagName: tagName,
attributes: attributes,
argument: argument,
selfClosed: selfClosed,
dynamicAttributes: dynamicAttributes
});
var endTag;
if (!openTagOnly) {
endTag = new EndTag({
tagName: tagName
});
}
if (bodyOnlyIf) {
var startIf = builder.ifStatement(builder.negate(bodyOnlyIf), [
startTag
]);
var endIf = builder.ifStatement(builder.negate(bodyOnlyIf), [
endTag
]);
return [
startIf,
body,
endIf
];
} else {
if (openTagOnly) {
return codegen.generateCode(startTag);
} else {
return [
startTag,
body,
endTag
];
}
}
};

View File

@ -0,0 +1,158 @@
'use strict';
var Node = require('../Node');
var Literal = require('../Literal');
var HtmlAttributeCollection = require('../HtmlAttributeCollection');
var generateHTMLCode = require('./html/generateCode');
var generateVDOMCode = require('./vdom/generateCode');
var vdomUtil = require('../../util/vdom');
function beforeGenerateCode(event) {
if (event.node.tagName === 'script') {
event.context.pushFlag('SCRIPT_BODY');
}
}
function afterGenerateCode(event) {
if (event.node.tagName === 'script') {
event.context.popFlag('SCRIPT_BODY');
}
}
class HtmlElement extends Node {
constructor(def) {
super('HtmlElement');
this.tagName = null;
this.tagNameExpression = null;
this.setTagName(def.tagName);
this._attributes = def.attributes;
this.body = this.makeContainer(def.body);
this.argument = def.argument;
if (!(this._attributes instanceof HtmlAttributeCollection)) {
this._attributes = new HtmlAttributeCollection(this._attributes);
}
this.openTagOnly = def.openTagOnly;
this.selfClosed = def.selfClosed;
this.dynamicAttributes = undefined;
this.bodyOnlyIf = undefined;
this.on('beforeGenerateCode', beforeGenerateCode);
this.on('afterGenerateCode', afterGenerateCode);
}
generateHTMLCode(codegen) {
return generateHTMLCode(this, codegen);
}
generateVDOMCode(codegen) {
return generateVDOMCode(this, codegen, vdomUtil);
}
addDynamicAttributes(expression) {
if (!this.dynamicAttributes) {
this.dynamicAttributes = [];
}
this.dynamicAttributes.push(expression);
}
getAttribute(name) {
return this._attributes != null && this._attributes.getAttribute(name);
}
getAttributeValue(name) {
var attr = this._attributes != null && this._attributes.getAttribute(name);
if (attr) {
return attr.value;
}
}
addAttribute(attr) {
this._attributes.addAttribute(attr);
}
setAttributeValue(name, value) {
this._attributes.setAttributeValue(name, value);
}
replaceAttributes(newAttributes) {
this._attributes.replaceAttributes(newAttributes);
}
removeAttribute(name) {
if (this._attributes) {
this._attributes.removeAttribute(name);
}
}
removeAllAttributes() {
this._attributes.removeAllAttributes();
}
hasAttribute(name) {
return this._attributes != null && this._attributes.hasAttribute(name);
}
getAttributes() {
return this._attributes.all;
}
get attributes() {
return this._attributes.all;
}
forEachAttribute(callback, thisObj) {
var attributes = this._attributes.all.concat([]);
for (let i=0, len=attributes.length; i<len; i++) {
callback.call(thisObj, attributes[i]);
}
}
setTagName(tagName) {
this.tagName = null;
this.tagNameExpression = null;
if (tagName instanceof Node) {
if (tagName instanceof Literal) {
this.tagName = tagName.value;
this.tagNameExpression = tagName;
} else {
this.tagNameExpression = tagName;
}
} else if (typeof tagName === 'string') {
this.tagNameExpression = new Literal({value: tagName});
this.tagName = tagName;
}
}
isLiteralTagName() {
return this.tagName != null;
}
toJSON() {
return {
type: this.type,
tagName: this.tagName,
attributes: this._attributes,
argument: this.argument,
body: this.body,
bodyOnlyIf: this.bodyOnlyIf,
dynamicAttributes: this.dynamicAttributes
};
}
setBodyOnlyIf(condition) {
this.bodyOnlyIf = condition;
}
walk(walker) {
this.setTagName(walker.walk(this.tagNameExpression));
this._attributes.walk(walker);
this.body = walker.walk(this.body);
}
}
module.exports = HtmlElement;

View File

@ -0,0 +1,186 @@
'use strict';
const Node = require('../../Node');
const vdomUtil = require('../../../util/vdom');
function finalizeCreateArgs(createArgs, builder) {
var length = createArgs.length;
var lastArg;
for (var i=length-1; i>=0; i--) {
var arg = createArgs[i];
if (arg) {
lastArg = arg;
} else {
if (lastArg != null) {
createArgs[i] = builder.literalNull();
} else {
length--;
}
}
}
createArgs.length = length;
return createArgs;
}
class HtmlElementVDOM extends Node {
constructor(def) {
super('HtmlElementVDOM');
this.tagName = def.tagName;
this.isStatic = def.isStatic;
this.isAttrsStatic = def.isAttrsStatic;
this.isHtmlOnly = def.isHtmlOnly;
this.attributes = def.attributes;
this.body = def.body;
this.dynamicAttributes = def.dynamicAttributes;
this.isChild = false;
this.createElementId = undefined;
this.attributesArg = undefined;
this.nextConstId = undefined;
}
generateCode(codegen) {
let context = codegen.context;
let builder = codegen.builder;
// When there are any VDOM nodes in the AST then we need to optimize the intermediate AST
// before the final AST is returned. We use the "afterTemplateRootBodyGenerated" event
// to finalize the VDOM AST nodes.
vdomUtil.attachEventListeners(context);
let attributes = this.attributes;
let dynamicAttributes = this.dynamicAttributes;
let attributesArg = null;
if (attributes && attributes.length) {
let addAttr = function(name, value) {
if (!attributesArg) {
attributesArg = {};
}
if (value.type === 'Literal') {
let literalValue = value.value;
if (literalValue == null || literalValue === false) {
return;
} else if (typeof literalValue === 'number') {
value.value = literalValue.toString();
}
}
attributesArg[name] = value;
};
attributes.forEach((attr) => {
let value = attr.value;
if (!attr.name) {
return;
}
addAttr(attr.name, value);
});
if (attributesArg) {
attributesArg = builder.literal(attributesArg);
}
}
if (dynamicAttributes && dynamicAttributes.length) {
dynamicAttributes.forEach((attrs) => {
if (attributesArg) {
let mergeVar = context.helper('merge');
attributesArg = builder.functionCall(mergeVar, [
attributesArg, // Input props from the attributes take precedence
attrs
]);
} else {
attributesArg = attrs;
}
});
}
this.attributesArg = attributesArg;
return this;
}
walk(walker) {
this.tagName = walker.walk(this.tagName);
this.attributes = walker.walk(this.attributes);
this.body = walker.walk(this.body);
}
writeCode(writer) {
let builder = writer.builder;
let body = this.body;
let attributesArg = this.attributesArg;
let nextConstId = this.nextConstId;
let childCount = body && body.length;
let createArgs = new Array(4); // tagName, attributes, childCount, const ID
createArgs[0] = this.tagName;
if (attributesArg) {
createArgs[1] = attributesArg;
}
if (childCount != null) {
createArgs[2] = builder.literal(childCount);
}
if (nextConstId) {
createArgs[3] = nextConstId;
}
// Remove trailing undefined arguments and convert non-trailing
// undefined elements to a literal null node
createArgs = finalizeCreateArgs(createArgs, builder);
let funcCall;
if (this.isChild) {
writer.write('.');
funcCall = builder.functionCall(
builder.identifier('e'),
createArgs);
} else if (this.isStatic) {
funcCall = builder.functionCall(
this.createElementId,
createArgs);
} else if (this.isHtmlOnly) {
writer.write('out.');
funcCall = builder.functionCall(
builder.identifier('e'),
createArgs);
} else {
writer.write('out.');
funcCall = builder.functionCall(
builder.identifier('be'),
createArgs);
}
writer.write(funcCall);
if (body && body.length) {
writer.incIndent();
for(let i=0; i<body.length; i++) {
let child = body[i];
child.isChild = true;
writer.write('\n');
writer.writeLineIndent();
writer.write(child);
}
writer.decIndent();
}
}
}
module.exports = HtmlElementVDOM;

View File

@ -0,0 +1,55 @@
'use strict';
var HtmlElementVDOM = require('./HtmlElementVDOM');
function checkAttributesStatic(attributes) {
if (attributes) {
for (let i=0; i<attributes.length; i++) {
let attr = attributes[i];
if (!attr.isStatic) {
return false;
}
}
}
return true;
}
module.exports = function(node, codegen, vdomUtil) {
var body = codegen.generateCode(node.body);
var tagName = codegen.generateCode(node.tagNameExpression);
var attributes = codegen.generateCode(node.getAttributes());
var dynamicAttributes = codegen.generateCode(node.dynamicAttributes);
var isAttrsStatic = checkAttributesStatic(attributes);
var isStatic = isAttrsStatic && node.isLiteralTagName();
var isHtmlOnly = true;
if (body && body.length) {
for (var i=0; i<body.length; i++) {
let child = body[i];
if (child.type === 'HtmlElementVDOM' || child.type === 'TextVDOM') {
if (!child.isHtmlOnly) {
isStatic = false;
isHtmlOnly = false;
} if (!child.isStatic) {
isStatic = false;
}
} else {
isHtmlOnly = false;
isStatic = false;
}
}
}
return new HtmlElementVDOM({
tagName,
attributes,
body,
isStatic,
isAttrsStatic,
isHtmlOnly,
dynamicAttributes
});
};

View File

@ -193,6 +193,7 @@ class Node {
delete result._events;
delete result._finalNode;
delete result._trimStartEnd;
delete result._childTextNormalized;
return result;
}

View File

@ -24,6 +24,18 @@ class TemplateRoot extends Node {
var body = codegen.generateCode(this.body);
var templateRootBodyEvent = {
body,
context
};
// Emit an event to give code generators one more chance to optimize/finalize the AST
// before the final AST is returned. This VDOM AST nodes use this event to otpimize
// the AST by separating out static subtrees
context.emit('afterTemplateRootBodyGenerated', templateRootBodyEvent);
body = templateRootBodyEvent.body;
var builder = codegen.builder;
let renderStatements = [];
@ -48,9 +60,7 @@ class TemplateRoot extends Node {
builder.identifierOut()
],
renderStatements)
]);
} else {
let createStatements = [];
let staticNodes = context.getStaticNodes();

View File

@ -1,88 +0,0 @@
'use strict';
var ok = require('assert').ok;
var Node = require('./Node');
var Literal = require('./Literal');
var escapeXml = require('raptor-util/escapeXml');
class Text extends Node {
constructor(def) {
super('Text');
this.argument = def.argument;
this.escape = def.escape !== false;
this.normalized = false;
this.isFirst = false;
this.isLast = false;
this.preserveWhitespace = def.preserveWhitespace === true;
ok(this.argument, 'Invalid argument');
}
isLiteral() {
return this.argument instanceof Node && this.argument.type === 'Literal';
}
generateHTMLCode(codegen) {
var context = codegen.context;
var argument = this.argument;
var escape = this.escape !== false;
if (argument instanceof Literal) {
if (!argument.value) {
return null;
}
if (context.isFlagSet('SCRIPT_BODY')) {
escape = false;
}
if (escape === true) {
argument.value = escapeXml(argument.value.toString());
}
} else {
let builder = codegen.builder;
if (escape) {
let escapeIdentifier = context.helper('escapeXml');
if (context.isFlagSet('SCRIPT_BODY')) {
escapeIdentifier = context.helper('escapeScript');
}
// TODO Only escape the parts that need to be escaped if it is a compound expression with static
// text parts
argument = builder.functionCall(
escapeIdentifier,
[argument]);
} else {
argument = builder.functionCall(context.helper('str'), [ argument ]);
}
}
return codegen.builder.html(argument);
}
isWhitespace() {
var argument = this.argument;
return (argument instanceof Literal) &&
(typeof argument.value === 'string') &&
(argument.value.trim() === '');
}
appendText(text) {
if (!this.isLiteral()) {
throw new Error('Text cannot be appended to a non-literal Text node');
}
this.argument.value += text;
}
toJSON() {
return {
type: this.type,
argument: this.argument
};
}
}
module.exports = Text;

View File

@ -0,0 +1,61 @@
'use strict';
var escapeXml = require('raptor-util/escapeXml');
var Literal = require('../..//Literal');
module.exports = function(node, codegen) {
var context = codegen.context;
var argument = codegen.generateCode(node.argument);
var escape = node.escape !== false;
var htmlArray = [];
function append(argument) {
if (argument instanceof Literal) {
if (!argument.value) {
return;
}
if (context.isFlagSet('SCRIPT_BODY')) {
escape = false;
}
if (escape === true) {
argument.value = escapeXml(argument.value.toString());
}
htmlArray.push(argument);
} else {
let builder = codegen.builder;
if (escape) {
let escapeIdentifier = context.helper('escapeXml');
if (context.isFlagSet('SCRIPT_BODY')) {
escapeIdentifier = context.helper('escapeScript');
}
// TODO Only escape the parts that need to be escaped if it is a compound expression with static
// text parts
argument = builder.functionCall(
escapeIdentifier,
[argument]);
} else {
argument = builder.functionCall(context.helper('str'), [ argument ]);
}
htmlArray.push(argument);
}
}
if (Array.isArray(argument)) {
argument.forEach(append);
} else {
append(argument);
}
if (htmlArray.length) {
return codegen.builder.html(htmlArray);
} else {
return null;
}
};

View File

@ -0,0 +1,85 @@
'use strict';
var ok = require('assert').ok;
var Node = require('../Node');
var Literal = require('../Literal');
var generateHTMLCode = require('./html/generateCode');
var generateVDOMCode = require('./vdom/generateCode');
var vdomUtil = require('../../util/vdom');
class Text extends Node {
constructor(def) {
super('Text');
this.argument = def.argument;
this.escape = def.escape !== false;
this.normalized = false;
this.isFirst = false;
this.isLast = false;
this.preserveWhitespace = def.preserveWhitespace === true;
ok(this.argument, 'Invalid argument');
}
generateHTMLCode(codegen) {
return generateHTMLCode(this, codegen);
}
generateVDOMCode(codegen) {
return generateVDOMCode(this, codegen, vdomUtil);
}
isLiteral() {
return this.argument instanceof Node && this.argument.type === 'Literal';
}
isWhitespace() {
var argument = this.argument;
return (argument instanceof Literal) &&
(typeof argument.value === 'string') &&
(argument.value.trim() === '');
}
// _append(appendArgument) {
// var argument = this.argument;
//
// if (Array.isArray(argument)) {
// var len = argument.length;
// var last = argument[len-1];
//
// if (last instanceof Literal && appendArgument instanceof Literal) {
// last.value += appendArgument.value;
// } else {
// this.argument.push(appendArgument);
// }
// } else {
// if (argument instanceof Literal && appendArgument instanceof Literal) {
// argument.value += appendArgument.value;
// } else {
// this.argument = [ this.argument, appendArgument ];
// }
// }
// }
//
// append(text) {
// var appendArgument = text.argument;
// if (!appendArgument) {
// return;
// }
//
// if (Array.isArray(appendArgument)) {
// appendArgument.forEach(this._append, this);
// } else {
// this._append(appendArgument);
// }
// }
toJSON() {
return {
type: this.type,
argument: this.argument
};
}
}
module.exports = Text;

View File

@ -0,0 +1,87 @@
'use strict';
const Node = require('../../Node');
const Literal = require('../../Literal');
const vdomUtil = require('../../../util/vdom');
class TextVDOM extends Node {
constructor(def) {
super('TextVDOM');
this.arguments = [def.argument];
this.isStatic = def.isStatic;
this.isHtmlOnly = true;
this.isChild = false;
this.createTextId = undefined;
}
generateCode(codegen) {
var context = codegen.context;
// When there are any VDOM nodes in the AST then we need to optimize the intermediate AST
// before the final AST is returned. We use the "afterTemplateRootBodyGenerated" event
// to finalize the VDOM AST nodes.
vdomUtil.attachEventListeners(context);
return this;
}
_append(appendArgument) {
let args = this.arguments;
let len = args.length;
let last = args[len-1];
if (last instanceof Literal && appendArgument instanceof Literal) {
last.value += appendArgument.value;
} else {
args.push(appendArgument);
}
}
append(textVDOMToAppend) {
if (!textVDOMToAppend.isStatic) {
this.isStatic = false;
}
textVDOMToAppend.arguments.forEach(this._append, this);
}
writeCode(writer) {
let builder = writer.builder;
let args = this.arguments;
let textArg = args[0];
for (let i=1; i<args.length; i++) {
textArg = builder.binaryExpression(textArg, '+', args[i]);
}
if (this.isChild) {
let funcCall = builder.functionCall(
builder.identifier('t'),
[
textArg
]);
writer.write('.');
writer.write(funcCall);
} else if (this.isStatic) {
let funcCall = builder.functionCall(
this.createTextId,
[
textArg
]);
writer.write(funcCall);
} else {
let funcCall = builder.functionCall(
builder.identifier('t'),
[
textArg
]);
writer.write('out.');
writer.write(funcCall);
}
}
}
module.exports = TextVDOM;

View File

@ -0,0 +1,16 @@
'use strict';
var TextVDOM = require('./TextVDOM');
var Literal = require('../../Literal');
module.exports = function(node, codegen, vdomUtil) {
var argument = codegen.generateCode(node.argument);
if (argument instanceof Literal && argument.value === '') {
// Don't add empty text nodes to the final tree
return null;
}
var isStatic = vdomUtil.isStaticValue(argument);
return new TextVDOM({ argument, isStatic });
};

View File

@ -133,7 +133,6 @@ function createInlineCompiler(filename, userOptions) {
var compiler = defaultCompiler;
var context = new CompileContext('', filename, compiler.builder, options);
return new InlineCompiler(context, compiler);
}

View File

@ -0,0 +1,7 @@
var crypto = require('crypto');
module.exports = function(str) {
var shasum = crypto.createHash('sha1');
shasum.update(str);
return shasum.digest('hex');
};

View File

@ -0,0 +1,161 @@
'use strict';
/*
Algorithm:
Walk the DOM tree to find all HtmlElementVDOM and TextVDOM nodes
a) If a node is static then move to a static variable. Depending on whether or not the node is a root or nested,
we will need to replace it with one of the following:
- out.n(staticVar)
- .n(staticVar)
b) If a node is HTML-only then generate code depending on if it is root or not:
- out.e('div', ...) | out.t('foo')
- .e('div', ...) || .t('foo')
c) Else, generate one of the following:
- out.beginElement()
*/
const Node = require('../../ast/Node');
const nextConstIdFuncSymbol = Symbol();
class NodeVDOM extends Node {
constructor(variableIdentifier) {
super('NodeVDOM');
this.variableIdentifier = variableIdentifier;
}
writeCode(writer) {
var builder = writer.builder;
let funcCall = builder.functionCall(
builder.identifier('n'),
[
this.variableIdentifier
]);
if (this.isChild) {
writer.write('.');
} else {
writer.write('out.');
}
writer.write(funcCall);
}
}
class EndElementNode extends Node {
constructor() {
super('EndElementNode');
}
writeCode(writer) {
writer.write('out.ee()');
}
}
function finalizeVDOMNodes(nodes, context) {
let builder = context.builder;
let nextNodeId = 0;
let nextAttrsId = 0;
function generateStaticNode(node) {
if (node.type === 'HtmlElementVDOM') {
node.createElementId = context.importModule('marko_createElement', 'marko/vdom/createElement');
} else {
node.createTextId = context.importModule('marko_createText', 'marko/vdom/createText');
}
let nextConstIdFunc = context.data[nextConstIdFuncSymbol];
if (!nextConstIdFunc) {
let constId = context.importModule('marko_const', 'marko/runtime/vdom/const');
let fingerprintLiteral = builder.literal(context.getFingerprint(6));
nextConstIdFunc = context.data[nextConstIdFuncSymbol] = context.addStaticVar('marko_const_nextId', builder.functionCall(constId, [ fingerprintLiteral ]));
}
node.nextConstId = builder.functionCall(nextConstIdFunc, []);
node.isStaticRoot = true;
let staticNodeId = context.addStaticVar('marko_node' + (nextNodeId++), node);
return new NodeVDOM(staticNodeId);
}
function handleStaticAttributes(node) {
var attributesArg = node.attributesArg;
if (attributesArg) {
node.isStaticRoot = true;
let staticAttrsId = context.addStaticVar('marko_attrs' + (nextAttrsId++), attributesArg);
node.attributesArg = staticAttrsId;
}
}
function generateNodesForArray(nodes) {
let finalNodes = [];
let i = 0;
while (i<nodes.length) {
let node = nodes[i];
if (node.type === 'HtmlElementVDOM') {
if (node.isStatic) {
finalNodes.push(generateStaticNode(node));
} else {
if (node.isAttrsStatic) {
handleStaticAttributes(node);
}
if (node.isHtmlOnly) {
finalNodes.push(node);
} else {
finalNodes.push(node);
finalNodes = finalNodes.concat(generateNodesForArray(node.body));
node.body = null;
finalNodes.push(new EndElementNode());
}
}
} else if (node.type === 'TextVDOM') {
let firstTextNode = node;
// We will need to merge the text nodes into a single node
while(++i<nodes.length) {
let currentTextNode = nodes[i];
if (currentTextNode.type === 'TextVDOM') {
firstTextNode.append(currentTextNode);
} else {
break;
}
}
// if (firstTextNode.isStatic) {
// finalNodes.push(generateStaticNode(firstTextNode));
// continue;
// } else {
// finalNodes.push(firstTextNode);
// }
firstTextNode.isStatic = false;
finalNodes.push(firstTextNode);
continue;
} else {
finalNodes.push(node);
}
i++;
}
return finalNodes;
}
let walker = context.createWalker({
enterArray(nodes) {
return generateNodesForArray(nodes);
}
});
return walker.walk(nodes);
}
module.exports = finalizeVDOMNodes;

View File

@ -0,0 +1,18 @@
var finalizeVDOMNodes = require('./finalizeVDOMNodes');
var isStaticValue = require('./isStaticValue');
var vdomEventListenersAttached = Symbol();
function attachEventListeners(context) {
var data = context.data;
if (!data[vdomEventListenersAttached]) {
data[vdomEventListenersAttached] = true;
context.on('afterTemplateRootBodyGenerated', function(event) {
event.body = finalizeVDOMNodes(event.body, context);
});
}
}
exports.finalizeVDOMNodes = finalizeVDOMNodes;
exports.isStaticValue = isStaticValue;
exports.attachEventListeners = attachEventListeners;

View File

@ -0,0 +1,50 @@
'use strict';
var Literal = require('../../ast/Literal');
var Node = require('../../ast/Node');
function isStaticArray(array) {
for (let i=0; i<array.length; i++) {
if (!isStaticValue(array[i])) {
return false;
}
}
return true;
}
function isStaticObject(object) {
for (var k in object) {
if (object.hasOwnProperty(k)) {
let v = object[k];
if (!isStaticValue(v)) {
return false;
}
}
}
}
function isStaticValue(value) {
if (value == null) {
return true;
}
if (value instanceof Node) {
if (value instanceof Literal) {
return isStaticValue(value.value);
} else {
return false;
}
} else {
if (typeof value === 'object') {
if (Array.isArray(value)) {
return isStaticArray(value);
} else {
return isStaticObject(value);
}
} else {
return true;
}
}
}
module.exports = isStaticValue;

View File

@ -36,6 +36,7 @@
"events": "^1.0.2",
"htmljs-parser": "^1.5.3",
"lasso-package-root": "^1.0.0",
"marko-vdom": "^0.3.0",
"minimatch": "^3.0.2",
"object-assign": "^4.1.0",
"property-handlers": "^1.0.0",

View File

@ -109,7 +109,7 @@ Template.prototype = {
if (localData.$global) {
out.global = extend(out.global, localData.$global);
delete localData.$global;
localData.$global = null;
}
this._(localData, out);
@ -145,7 +145,7 @@ Template.prototype = {
if ((globalData = data.$global)) {
// We will *move* the "$global" property
// into the "out.global" object
delete data.$global;
data.$global = null;
}
} else {
finalData = {};

6
runtime/vdom/const.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = function(id) {
var i=0;
return function() {
return id + (i++);
};
};

View File

@ -0,0 +1,11 @@
function create(__markoHelpers) {
return function render(data, out) {
out.e("div", {
foo: "bar",
hello: "world"
}, 1)
.t(("Hello " + name) + "!");
};
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -0,0 +1,3 @@
<div ${ { foo: 'bar', hello: 'world' } }>
Hello ${name}!
</div>

View File

@ -0,0 +1,13 @@
function create(__markoHelpers) {
return function render(data, out) {
var attrs = {
foo: "bar",
hello: "world"
};
out.e("div", attrs, 1)
.t(("Hello " + name) + "!");
};
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -0,0 +1,5 @@
var attrs={ foo: 'bar', hello: 'world' }
<div ${attrs}>
Hello ${name}!
</div>

View File

@ -0,0 +1,12 @@
function create(__markoHelpers) {
var marko_attrs0 = {
"class": "foo"
};
return function render(data, out) {
out.e("div", marko_attrs0, 1)
.t(("Hello " + name) + "!");
};
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -0,0 +1,3 @@
<div.foo>
Hello ${name}!
</div>

View File

@ -0,0 +1,28 @@
function create(__markoHelpers) {
var marko_forEach = __markoHelpers.f,
marko_createElement = require("marko/vdom/createElement"),
marko_const = require("marko/runtime/vdom/const"),
marko_const_nextId = marko_const("733fee"),
marko_node0 = marko_createElement("div", null, 1, marko_const_nextId())
.t("No colors!");
return function render(data, out) {
out.e("h1", null, 1)
.t(("Hello " + data.name) + "!");
if (data.colors.length) {
out.be("ul");
marko_forEach(data.colors, function(color) {
out.e("li", null, 1)
.t(color);
});
out.ee();
} else {
out.n(marko_node0);
}
};
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -0,0 +1,12 @@
<h1>
Hello ${data.name}!
</h1>
<ul if(data.colors.length)>
<li for(color in data.colors)>
${color}
</li>
</ul>
<div else>
No colors!
</div>

View File

@ -0,0 +1,19 @@
function create(__markoHelpers) {
var marko_createElement = require("marko/vdom/createElement"),
marko_const = require("marko/runtime/vdom/const"),
marko_const_nextId = marko_const("69a896"),
marko_node0 = marko_createElement("div", {
"class": "hello",
onclick: "onClick()"
}, 1, marko_const_nextId())
.t("Welcome!");
return function render(data, out) {
out.e("span", null, 2)
.e("h1", null, 1)
.t(("Hello " + data.name) + "!")
.n(marko_node0);
};
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -0,0 +1,6 @@
<span>
<h1>Hello ${data.name}!</h1>
<div class="hello" onclick="onClick()">
Welcome!
</div>
</span>

View File

@ -0,0 +1,16 @@
function create(__markoHelpers) {
var marko_createElement = require("marko/vdom/createElement"),
marko_const = require("marko/runtime/vdom/const"),
marko_const_nextId = marko_const("0524f9"),
marko_node0 = marko_createElement("div", {
"class": "hello",
onclick: "onClick()"
}, 1, marko_const_nextId())
.t("Hello World!");
return function render(data, out) {
out.n(marko_node0);
};
}
(module.exports = require("marko").c(__filename)).c(create);

View File

@ -0,0 +1,3 @@
<div class="hello" onclick="onClick()">
Hello World!
</div>

View File

@ -19,7 +19,7 @@ function createCodeGenerator(context) {
}
function createCodeWriter(context) {
return new CodeWriter(context);
return new CodeWriter(context, builder);
}
describe('compiler/codegen', function() {

View File

@ -0,0 +1,50 @@
'use strict';
require('./patch-module');
var chai = require('chai');
chai.config.includeStack = true;
var path = require('path');
var compiler = require('../compiler');
var autotest = require('./autotest');
var fs = require('fs');
require('marko/node-require').install();
describe('vdom-compiler', function() {
var autoTestDir = path.join(__dirname, 'autotests/vdom-compiler');
autotest.scanDir(autoTestDir, function run(dir, helpers, done) {
var templatePath = path.join(dir, 'template.marko');
var mainPath = path.join(dir, 'test.js');
var main;
if (fs.existsSync(mainPath)) {
main = require(mainPath);
}
var compilerOptions = { output: 'vdom' };
if (main && main.checkError) {
var e;
try {
compiler.compileFile(templatePath, compilerOptions);
} catch(_e) {
e = _e;
}
if (!e) {
throw new Error('Error expected');
}
main.checkError(e);
done();
} else {
var compiledSrc = compiler.compileFile(templatePath, compilerOptions);
helpers.compare(compiledSrc, '.js');
done();
}
});
});

1
vdom.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('marko-vdom');