Migration stage (#1180)

* Add migration stage to taglib
* integrate migrations into migration stage
This commit is contained in:
Dylan Piercey 2018-12-03 15:35:19 -08:00 committed by Michael Rawlings
parent 3e68893388
commit 7d37fe3634
27 changed files with 501 additions and 269 deletions

View File

@ -460,10 +460,6 @@ class CompileContext extends EventEmitter {
elDef = { tagName, argument, attributes, openTagOnly, selfClosed };
}
if (elDef.tagName === "") {
elDef.tagName = tagName = "assign";
}
if (!attributes) {
attributes = elDef.attributes = [];
} else if (typeof attributes === "object") {

View File

@ -154,7 +154,7 @@ class Compiler {
var codeGenerator = new CodeGenerator(context);
// STAGE 1: Parse the template to produce the initial AST
var ast = this.parser.parse(src, context);
var ast = this.parser.parse(src, context, { migrate: true });
context._parsingFinished = true;
if (!context.ignoreUnrecognizedTags && context.unrecognizedTags) {

87
src/compiler/Migrator.js Normal file
View File

@ -0,0 +1,87 @@
"use strict";
var createError = require("raptor-util/createError");
const FLAG_MIGRATOR_APPLIED = "migratorApply";
function migrateNode(node, context) {
if (node.isDetached()) {
return; //The node might have been removed from the tree
}
if (!(node.tagName || node.tagNameExpression)) {
return;
}
context.taglibLookup.forEachTagMigrator(node, migration => {
// Check to make sure a migration of a certain type is only applied once to a node
if (node.isTransformerApplied(migration)) {
return;
}
// Mark the node as have been transformed by the current migrator
node.setTransformerApplied(migration);
// Set the flag to indicate that a node was transformed
context.setFlag(FLAG_MIGRATOR_APPLIED);
context._currentNode = node;
try {
migration(node, context);
} catch (e) {
throw createError(
new Error(
'Unable to migrate template at path "' +
context.filename +
'". Error: ' +
e.message
),
e
);
}
});
}
function migrateTreeHelper(node, context) {
migrateNode(node, context);
/*
* Now process the child nodes by looping over the child nodes
* and migrating the subtree recursively
*
* NOTE: The length of the childNodes array might change as the tree is being performed.
* The checks to prevent migrators from being applied multiple times makes
* sure that this is not a problem.
*/
node.forEachChild(function(childNode) {
migrateTreeHelper(childNode, context);
});
}
function migrateTree(ast, context) {
// TODO: Consider moving this into the loop below so that root level migrations
// are also run on new nodes.
context.taglibLookup.forEachTemplateMigrator(migration => {
migration(ast, context);
});
/*
* The tree is continuously migrated until we go through an entire pass where
* there were no new nodes that needed to be migrated. This loop makes sure that
* nodes added by migrators are also migrated.
*/
do {
context.clearFlag(FLAG_MIGRATOR_APPLIED);
//Reset the flag to indicate that no migrations were yet applied to any of the nodes for this pass
migrateTreeHelper(ast, context); //Run the migrations on the tree
} while (context.isFlagSet(FLAG_MIGRATOR_APPLIED));
}
class Migrator {
migrate(ast, context) {
migrateTree(ast, context);
return ast;
}
}
module.exports = Migrator;

View File

@ -2,6 +2,7 @@
var ok = require("assert").ok;
var extend = require("raptor-util/extend");
var Normalizer = require("./Normalizer");
var Migrator = require("./Migrator");
var COMPILER_ATTRIBUTE_HANDLERS = {
"preserve-whitespace": function(attr, context) {
@ -96,6 +97,7 @@ class Parser {
var rootNode = builder.templateRoot();
var mergedOptions = Object.assign({}, this.defaultOptions, options);
var raw = mergedOptions.raw === true;
var migrate = mergedOptions.migrate === true;
this.stack.push({
node: rootNode
@ -103,6 +105,11 @@ class Parser {
this.parserImpl.parse(src, this, context.filename, mergedOptions);
if (migrate) {
var migrator = new Migrator();
rootNode = migrator.migrate(rootNode, context);
}
if (!raw) {
var normalizer = new Normalizer();
rootNode = normalizer.normalize(rootNode, context);
@ -143,6 +150,10 @@ class Parser {
argument = argument.value;
}
if (!el.tagNameExpression && !tagName) {
tagName = el.tagName = "assign";
}
if (tagName === "marko-compiler-options") {
this.parentNode.setTrimStartEnd(true);

View File

@ -40,7 +40,7 @@ class Node {
this.tagDef = null; // The tag definition associated with this Node
this._codeGeneratorFuncs = null;
this._flags = {};
this._transformersApplied = {};
this._transformersApplied = new Set();
this._preserveWhitespace = null;
this._events = null;
this._childTextNormalized = undefined;
@ -191,11 +191,11 @@ class Node {
}
isTransformerApplied(transformer) {
return this._transformersApplied[transformer.id] === true;
return this._transformersApplied.has(transformer);
}
setTransformerApplied(transformer) {
this._transformersApplied[transformer.id] = true;
this._transformersApplied.add(transformer);
}
toString() {

View File

@ -273,6 +273,10 @@ var coreTaglibsRegistered = false;
function registerCoreTaglibs() {
if (!coreTaglibsRegistered) {
coreTaglibsRegistered = true;
registerTaglib(
require("../taglibs/migrate/marko.json"),
require.resolve("../taglibs/migrate/marko.json")
);
registerTaglib(
require("../taglibs/core/marko.json"),
require.resolve("../taglibs/core/marko.json")

View File

@ -23,6 +23,8 @@ class Tag {
this.dir = path.dirname(filePath);
}
this.migrators = {};
this.migratorPaths = [];
this.attributes = {};
this.transformers = {};
this.patternAttributes = [];
@ -198,6 +200,16 @@ class Tag {
hasNestedTags() {
return this.nestedTags != null;
}
forEachMigrator(callback, thisObj) {
this.migratorPaths
.map(function(path) {
return (this.migrators[path] =
this.migrators[path] || markoModules.require(path));
}, this)
.forEach(callback, thisObj);
}
getNodeFactory() {
var nodeFactory = this._nodeFactory;
if (nodeFactory !== undefined) {

View File

@ -3,6 +3,7 @@ var forEachEntry = require("raptor-util/forEachEntry");
var ok = require("assert").ok;
var path = require("path");
var loaders = require("./loaders");
var markoModules = require("../modules");
function handleImport(taglib, importedTaglib) {
var importsLookup = taglib.importsLookup || (taglib.importsLookup = {});
@ -65,6 +66,14 @@ class Taglib {
}
return attribute;
}
getMigrator() {
var path = this.migratorPath;
if (path) {
return (this._migrator =
this._migrator || markoModules.require(path));
}
}
addTag(tag) {
ok(arguments.length === 1, "Invalid args");
if (!tag.name) {

View File

@ -389,6 +389,16 @@ class TagLoader {
);
}
/**
* A custom tag can be mapped to module that is used
* migrate deprecated features to modern features.
*/
migrator(value) {
var tag = this.tag;
var dirname = this.dirname;
tag.migratorPaths.push(markoModules.resolveFrom(dirname, value));
}
/**
* A custom tag can be mapped to module that is is used
* to generate compile-time code for the custom tag. A

View File

@ -322,6 +322,18 @@ class TaglibLoader {
}
}
/**
* A taglib can be mapped to module that is used
* migrate deprecated features to modern features across the entire template.
*/
migrator(value) {
var taglib = this.taglib;
var dirname = this.dirname;
var path = markoModules.resolveFrom(dirname, value);
taglib.migratorPath = path;
}
textTransformer(value) {
// Marko allows a "text-transformer" to be registered. The provided
// text transformer will be called for any static text found in a template.

View File

@ -326,6 +326,59 @@ class TaglibLookup {
return attrDef;
}
forEachTemplateMigrator(callback, thisObj) {
for (var key in this.taglibsById) {
var migration = this.taglibsById[key].getMigrator();
if (migration) {
callback.call(thisObj, migration);
}
}
}
forEachTagMigrator(element, callback, thisObj) {
if (typeof element === "string") {
element = {
tagName: element
};
}
var tagName = element.tagName;
/*
* If the node is an element node then we need to find all matching
* migrators based on the URI and the local name of the element.
*/
var migrators = [];
function addMigrator(migrator) {
if (typeof migrator !== "function") {
throw new Error("Invalid transformer");
}
migrators.push(migrator);
}
/*
* Handle all of the migrators for all possible matching migrators.
*
* Start with the least specific and end with the most specific.
*/
if (this.merged.tags) {
if (tagName) {
if (this.merged.tags[tagName]) {
this.merged.tags[tagName].forEachMigrator(addMigrator);
}
}
if (this.merged.tags["*"]) {
this.merged.tags["*"].forEachMigrator(addMigrator);
}
}
migrators.forEach(callback, thisObj);
}
forEachTemplateTransformer(callback, thisObj) {
var transformers = this.merged.transformers;
if (transformers && transformers.length) {

View File

@ -1,30 +0,0 @@
module.exports = function codeGenerator(elNode, context) {
const attributes = elNode.attributes;
const builder = context.builder;
context.deprecate(
'The "<assign>" tag is deprecated. Please use "$ <js_code>" for JavaScript in the template. See: https://github.com/marko-js/marko/wiki/Deprecation:-var-assign-invoke-tags'
);
if (!attributes) {
context.addError(
"Invalid <assign> tag. Argument is missing. Example; <assign x=123 />"
);
return elNode;
}
elNode.replaceWith(
builder.scriptlet({
value: builder.parseExpression(
elNode.attributes
.map(
attr =>
attr.value == null
? attr.name
: `${attr.name} = ${attr.rawValue}`
)
.join(", ")
)
})
);
};

View File

@ -1,125 +1,6 @@
"use strict";
var createLoopNode = require("./util/createLoopNode");
var coreAttrHandlers = [
[
"while",
function(attr, node) {
var whileArgument = attr.argument;
if (!whileArgument) {
return false;
}
var whileNode = this.builder.whileStatement(whileArgument);
node.wrapWith(whileNode);
}
],
[
"for",
function(attr, node) {
var forArgument = attr.argument;
if (!forArgument) {
return false;
}
var loopNode;
try {
loopNode = createLoopNode(forArgument, null, this.builder);
} catch (e) {
if (e.code === "INVALID_FOR") {
this.addError(e.message);
return;
} else {
throw e;
}
}
//Surround the existing node with the newly created loop node
// NOTE: The loop node will be one of the following:
// ForEach, ForRange, ForEachProp or ForStatement
node.wrapWith(loopNode);
}
],
[
"if",
function(attr, node) {
var ifArgument = attr.argument;
if (!ifArgument) {
return false;
}
var test;
try {
test = this.builder.parseExpression(ifArgument);
} catch (e) {
test = this.builder.literalFalse();
this.addError(
"Invalid expression for if statement:\n" + e.message
);
}
var ifNode = this.builder.ifStatement(test);
//Surround the existing node with an "If" node
node.wrapWith(ifNode);
}
],
[
"unless",
function(attr, node) {
var ifArgument = attr.argument;
if (!ifArgument) {
return false;
}
var test;
try {
test = this.builder.parseExpression(ifArgument);
} catch (e) {
test = this.builder.literalFalse();
this.addError(
"Invalid expression for unless statement:\n" + e.message
);
}
test = this.builder.negate(test);
var ifNode = this.builder.ifStatement(test);
//Surround the existing node with an "if" node
node.wrapWith(ifNode);
}
],
[
"else-if",
function(attr, node) {
var elseIfArgument = attr.argument;
if (!elseIfArgument) {
return false;
}
var test;
try {
test = this.builder.parseExpression(elseIfArgument);
} catch (e) {
test = this.builder.literalFalse();
this.addError(
"Invalid expression for else-if statement:\n" + e.message
);
}
var elseIfNode = this.builder.elseIfStatement(test);
//Surround the existing node with an "ElseIf" node
node.wrapWith(elseIfNode);
}
],
[
"else",
function(attr, node) {
var elseNode = this.builder.elseStatement();
//Surround the existing node with an "Else" node
node.wrapWith(elseNode);
}
],
[
"body-only-if",
function(attr, node, el) {

View File

@ -1,10 +1,4 @@
{
"transformer": "./root-transformer",
"<assign>": {
"transformer": "./assign-tag",
"open-tag-only": true,
"deprecated": true
},
"<class>": {
"code-generator": "./class-tag",
"open-tag-only": true
@ -139,10 +133,6 @@
}
]
},
"<invoke>": {
"transformer": "./invoke-tag",
"deprecated": true
},
"<macro>": {
"node-factory": "./macro-tag",
"autocomplete": [
@ -213,10 +203,6 @@
}
]
},
"<var>": {
"transformer": "./var-tag",
"deprecated": true
},
"<while>": {
"code-generator": "./while-tag",
"autocomplete": [

View File

@ -1,76 +0,0 @@
"use strict";
const OUT_IDENTIFIER_REG = /[(,] *out *[,)]/;
const renderCallToDynamicTag = require("./util/renderCallToDynamicTag");
module.exports = function transform(el, context) {
const walker = context.createWalker({
enter(node) {
if (
node.type !== "Scriptlet" ||
!OUT_IDENTIFIER_REG.test(node.code)
) {
return;
}
const replacement = replaceScriptlets(
context.builder.parseStatement(node.code),
context
);
node.replaceWith(replacement);
}
});
walker.walk(el);
};
function replaceScriptlets(node, context) {
const builder = context.builder;
if (!node.type) {
if (node.replaceChild) {
node.forEach(child => {
const replacement = replaceScriptlets(child, context);
if (child !== replacement) {
node.replaceChild(replacement, child);
}
});
} else if (node.body) {
node.body.forEach(child => {
const replacement = replaceScriptlets(child, context);
if (child !== replacement) {
node.body.replaceChild(replacement, child);
}
});
}
return node;
}
switch (node.type) {
case "LogicalExpression":
node = builder.ifStatement(
node.operator === "&&" ? node.left : builder.negate(node.left),
[replaceScriptlets(node.right, context)]
);
break;
case "FunctionCall":
node = renderCallToDynamicTag(node, context) || node;
break;
case "If":
case "ElseIf":
node.body = replaceScriptlets(node.body, context);
if (node.else) {
replaceScriptlets(node.else, context);
}
break;
case "Else":
case "ForStatement":
case "WhileStatement":
node.body = replaceScriptlets(node.body, context);
break;
default:
break;
}
return node;
}

View File

@ -0,0 +1,33 @@
const printJS = require("./util/printJS");
const migrateControlFlowDirectives = require("./control-flow-directives");
module.exports = function migrator(elNode, context) {
const attributes = elNode.attributes;
const builder = context.builder;
migrateControlFlowDirectives(elNode, context);
elNode.setTransformerApplied(migrateControlFlowDirectives);
context.deprecate(
'The "<assign>" tag is deprecated. Please use "$ <js_code>" for JavaScript in the template. See: https://github.com/marko-js/marko/wiki/Deprecation:-var-assign-invoke-tags'
);
if (!attributes) {
context.addError(
"Invalid <assign> tag. Argument is missing. Example; <assign x=123 />"
);
return elNode;
}
elNode.attributes.forEach(attr => {
elNode.insertSiblingBefore(
builder.scriptlet({
value:
attr.value == null
? attr.name
: `${attr.name} = ${printJS(attr.value, context)}`
})
);
});
elNode.detach();
};

View File

@ -0,0 +1,39 @@
const CONTROL_FLOW_ATTRIBUTES = [
"while",
"for",
"if",
"unless",
"else-if",
"else"
];
module.exports = function migrate(el, context) {
const builder = context.builder;
if (CONTROL_FLOW_ATTRIBUTES.includes(el.tagName)) {
return;
}
el.forEachAttribute(attr => {
const name = attr.name;
if (
CONTROL_FLOW_ATTRIBUTES.includes(name) &&
(name === "else" || attr.argument)
) {
context.deprecate(
`The "${name}" attribute is deprecated. Please use the <${name}> tag instead. See: https://github.com/marko-js/marko/wiki/Deprecation:-control-flow-directive`
);
el.removeAttribute(name);
el.wrapWith(
builder.htmlElement(
name,
undefined,
[],
attr.argument,
false,
false
)
);
}
});
};

View File

@ -1,9 +1,12 @@
const renderCallToDynamicTag = require("./util/renderCallToDynamicTag");
const migrateControlFlowDirectives = require("./control-flow-directives");
module.exports = function codeGenerator(elNode, context) {
module.exports = function migrator(elNode, context) {
const builder = context.builder;
const functionAttr = elNode.attributes[0];
const functionArgs = functionAttr.argument;
migrateControlFlowDirectives(elNode, context);
elNode.setTransformerApplied(migrateControlFlowDirectives);
context.deprecate(
'The "<invoke>" tag is deprecated. Please use "$ <js_code>" for JavaScript in the template. See: https://github.com/marko-js/marko/wiki/Deprecation:-var-assign-invoke-tags'

View File

@ -0,0 +1,19 @@
{
"migrator": "./root-migrator",
"<*>": {
"migrator": "./control-flow-directives"
},
"<invoke>": {
"migrator": "./invoke-tag",
"deprecated": true
},
"<assign>": {
"migrator": "./assign-tag",
"open-tag-only": true,
"deprecated": true
},
"<var>": {
"migrator": "./var-tag",
"deprecated": true
}
}

View File

@ -0,0 +1,162 @@
"use strict";
const printJS = require("./util/printJS");
const OUT_IDENTIFIER_REG = /[(,] *out *[,)]/;
const renderCallToDynamicTag = require("./util/renderCallToDynamicTag");
module.exports = function migrator(el, context) {
const walker = context.createWalker({
enter(node) {
if (
node.type !== "Scriptlet" ||
!OUT_IDENTIFIER_REG.test(node.code)
) {
return;
}
let hasErrors;
let foundRenderCall;
const replacement = replaceScriptlets(
context.builder.parseStatement(node.code),
context
);
if (!foundRenderCall) {
return;
}
context.deprecate(
"Directly rendering by passing `out` to a function is deprecated. Please use the dynamic tag instead. See: https://github.com/marko-js/marko/wiki/Deprecation:-imperative-render-calls"
);
if (hasErrors) {
return;
}
if (!replacement.type) {
replacement.forEachChild(child =>
node.insertSiblingBefore(child)
);
node.detach();
} else {
node.replaceWith(replacement);
}
function replaceScriptlets(node, context) {
const builder = context.builder;
if (!node.type) {
if (node.replaceChild) {
node.forEach(child => {
const replacement = replaceScriptlets(
child,
context
);
if (child !== replacement) {
node.replaceChild(replacement, child);
}
});
} else if (node.body) {
node.body.forEach(child => {
const replacement = replaceScriptlets(
child,
context
);
if (child !== replacement) {
node.body.replaceChild(replacement, child);
}
});
}
return node;
}
switch (node.type) {
case "LogicalExpression":
node = builder.htmlElement(
"if",
undefined,
[replaceScriptlets(node.right, context)],
printJS(
node.operator === "&&"
? node.left
: builder.negate(node.left),
context
),
false,
false
);
break;
case "FunctionCall":
if (
node !==
(node =
renderCallToDynamicTag(node, context) || node)
) {
foundRenderCall = true;
}
break;
case "If":
node = builder.htmlElement(
"if",
undefined,
replaceScriptlets(node.body, context),
printJS(node.test, context),
false,
false
);
break;
case "ElseIf":
node = builder.htmlElement(
"else-if",
undefined,
replaceScriptlets(node.body, context),
printJS(node.test, context),
false,
false
);
break;
case "Else":
node = builder.htmlElement(
"else",
undefined,
replaceScriptlets(node.body, context),
undefined,
false,
false
);
break;
case "ForStatement":
node = builder.htmlElement(
"for",
undefined,
replaceScriptlets(node.body, context),
`${printJS(node.init, context)}; ${printJS(
node.test,
context
)}; ${printJS(node.update, context)}`,
false,
false
);
break;
case "WhileStatement":
node = builder.htmlElement(
"while",
undefined,
replaceScriptlets(node.body, context),
printJS(node.test, context),
false,
false
);
break;
default:
hasErrors = true;
break;
}
return node;
}
}
});
walker.walk(el);
};

View File

@ -0,0 +1,10 @@
const CodeWriter = require("../../../compiler/CodeWriter");
module.exports = function(node, context, options) {
const writer = new CodeWriter(
Object.assign({}, context.options, options),
context.builder
);
writer.write(node);
return writer.getCode();
};

View File

@ -1,3 +1,5 @@
const printJS = require("./printJS");
module.exports = function renderCallToDynamicTag(ast, context) {
const builder = context.builder;
const args = ast.args;
@ -58,7 +60,17 @@ module.exports = function renderCallToDynamicTag(ast, context) {
);
}
return context.createNodeForEl(tagName, tagAttrs, null, true, true);
const el = builder.htmlElement(
undefined,
tagAttrs,
undefined,
undefined,
true,
true
);
el.rawTagNameExpression = printJS(tagName, context);
return el;
};
function toAttributesOrSpread(val) {

View File

@ -1,4 +1,5 @@
const isValidJavaScriptVarName = require("../../compiler/util/isValidJavaScriptVarName");
const printJS = require("./util/printJS");
module.exports = function nodeFactory(elNode, context) {
const attributes = elNode.attributes;
@ -34,34 +35,31 @@ module.exports = function nodeFactory(elNode, context) {
lastChild.argument.value = lastChild.argument.value.trimRight();
}
const vars = elNode.attributes.map(attr => {
const scriptlets = elNode.attributes.map(attr => {
const name = attr.name;
const val = attr.rawValue;
if (!isValidJavaScriptVarName(attr.name)) {
if (!isValidJavaScriptVarName(name)) {
hasError = true;
context.addError(
"Invalid JavaScript variable name: " + attr.name,
"Invalid JavaScript variable name: " + name,
"INVALID_VAR_NAME"
);
return;
}
let parsedExpression = val;
if (val != null) {
parsedExpression = builder.parseExpression(val);
}
return builder.variableDeclarator(name, parsedExpression);
return builder.scriptlet({
value: `var ${
val == null ? name : `${name} = ${printJS(attr.value, context)}`
}`
});
});
if (hasError) {
return;
}
elNode.insertSiblingBefore(
builder.scriptlet({ value: builder.vars(vars) })
);
scriptlets.forEach(scriptlet => elNode.insertSiblingBefore(scriptlet));
elNode.forEachChild(node => elNode.insertSiblingBefore(node));
elNode.detach();
};

View File

@ -99,4 +99,4 @@
]
}
]
}
}

View File

@ -2,6 +2,7 @@
"name",
"age",
"foo-on-*",
"*",
"body-only-if",
"if",
"else-if",
@ -32,4 +33,4 @@
"on*",
"once*",
"w-on*"
]
]

View File

@ -3,7 +3,9 @@
"foo",
"bar",
"init-widgets",
"invoke",
"assign",
"var",
"class",
"else",
"else-if",
@ -13,7 +15,6 @@
"include",
"include-html",
"include-text",
"invoke",
"macro",
"macro-body",
"marko",
@ -21,7 +22,6 @@
"module-code",
"static",
"unless",
"var",
"while",
"layout-use",
"layout-put",
@ -239,4 +239,4 @@
"_preserve",
"no-update",
"widget-types"
]
]

View File

@ -13,9 +13,9 @@ function render(input, out, __component, component, state) {
var data = input;
var attrs = {
foo: "bar",
hello: "world"
}
foo: "bar",
hello: "world"
}
out.e("DIV", attrs, null, null, 3)
.t("Hello ")