diff --git a/lib/Vastly.global.js b/lib/Vastly.global.js index 06542b6d..ea0ac5ae 100644 --- a/lib/Vastly.global.js +++ b/lib/Vastly.global.js @@ -2,32 +2,36 @@ var Vastly = (function (exports) { 'use strict'; /** - * Get a node’s children + * Get a node’s children as an array * @param {object | object[]} node or nodes * @returns {object[]} */ - function children(node) { + function children (node) { if (Array.isArray(node)) { return node.flatMap(node => children(node)); } - return childProperties.flatMap(property => node[property] ?? []); + let nodeProperties = properties[node.type] ?? []; + return nodeProperties.flatMap(property => node[property] ?? []); } /** * Which properties of a node are child nodes? - * Can be imported and manipulated by calling code to extend the walker - * @type {string[]} + * Can be imported and modified by calling code to add support for custom node types. + * @type {Object.} */ - const childProperties = [ - "arguments", "callee", // CallExpression - "left", "right", // BinaryExpression, LogicalExpression - "argument", // UnaryExpression - "elements", // ArrayExpression - "test", "consequent", "alternate", // ConditionalExpression - "object", "property", // MemberExpression - "body" - ]; + const properties = children.properties = { + CallExpression: ["arguments", "callee"], + BinaryExpression: ["left", "right"], + UnaryExpression: ["argument"], + ArrayExpression: ["elements"], + ConditionalExpression: ["test", "consequent", "alternate"], + MemberExpression: ["object", "property"], + Compound: ["body"], + }; + + // Old JSEP versions + properties.LogicalExpression = properties.BinaryExpression; function closest (node, type) { let n = node; @@ -41,7 +45,7 @@ var Vastly = (function (exports) { return null; } - function matches(node, filter) { + function matches (node, filter) { if (!filter) { return true; } @@ -61,35 +65,155 @@ var Vastly = (function (exports) { /** * Recursively execute a callback on this node and all its children. - * If the callback returns a non-undefined value, it will overwrite the node. + * If the callback returns a non-undefined value, walking ends and the value is returned * @param {object} node * @param {function(object, string, object?)} callback * @param {object} [o] * @param {string | string[] | function} [o.only] Only walk nodes of this type * @param {string | string[] | function} [o.except] Ignore walking nodes of these types */ - function map (node, callback, o) { - return _map(node, callback, o); + function walk (node, callback, o) { + return _walk(node, callback, o); } - function _map (node, callback, o = {}, property, parent) { - let ignored = o.ignore && matches(node, o.ignore); + function _walk (node, callback, o = {}, property, parent) { + let ignored = o.except && matches(node, o.except); if (!ignored && matches(node, o.only)) { let ret = callback(node, property, parent); + if (ret !== undefined) { + // Callback returned a value, stop walking and return it + return ret; + } + for (let child of children(node)) { - _map(child, callback, o, property, node); + _walk(child, callback, o, property, node); } + } + } + + /** + * Set properties on each node pointing to its parent node. + * Required for many vastly functions, e.g. `closest()`. + * By default it will skip nodes that already have a `parent` property entirely, but you can set force = true to prevent that. + * @param {*} node + * @param {object} [options] + * @param {boolean} [options.force] Overwrite existing `parent` properties + */ + function setAll (node, options ) { + walk(node, (node, property, parent) => { + let ret = set(node, parent, options); + + if (ret === false) { + // We assume that if the node already has a parent, its subtree will also have parents + return false; + } + }); + } + + /** + * Set the `parent` property on a node. + * By default it will skip nodes that already have a `parent` property, but you can set force = true to prevent that. + * @param {object} node + * @param {object} parent + * @param {object} [options] + * @param {boolean} [options.force] Allow overwriting + */ + function set (node, parent, { force } = {}) { + if (!force && "parent" in node) { + // We assume that if the node already has a parent, its subtree will also have parents + return false; + } + else { + Object.defineProperty(node, "parent", { + value: parent, + enumerable: false, + configurable: true, + writable: true + }); + } + } - if (ret !== undefined && parent) { - // Callback returned a value, overwrite the node - // We apply such transformations after walking, to avoid infinite recursion - parent[property] = ret; + /** + * Get the parent node of a node. + * @param {object} node + * @returns {object | undefined} The parent node, or undefined if the node has no parent + */ + function get (node) { + return node.parent; + } + + var parents = /*#__PURE__*/Object.freeze({ + __proto__: null, + setAll: setAll, + set: set, + get: get + }); + + /** + * Recursively execute a callback on this node and all its children. + * If the callback returns a non-undefined value, it will overwrite the node. + * This function will not modify the root node of the input AST. + * + * @param {object | object[]} node AST node or array of nodes + * @param {Object. | function(object, string, object?, object) | (Object. | function(object, string, object?, object))[]} transformations A map of node types to callbacks, or a single callback that will be called for all node types, or a list of either, which will be applied in order + * @param {object} [o] + * @param {string | string[] | function} [o.only] Only walk nodes of this type + * @param {string | string[] | function} [o.except] Ignore walking nodes of these types + * @returns {object | object[]} The transformation's return value on the root node(s) of the input AST, or the root node(s) if the transformation did not return a value + */ + function transform (node, transformations, o) { + if (!Array.isArray(transformations)) { + transformations = [transformations]; + } + return _transform(node, transformations, o); + } + + function _transform (node, transformations, o = {}, property, parent) { + if (Array.isArray(node)) { + return node.map(n => _transform(n, transformations, o, property, parent)); + } + + const ignore = o.except && matches(node, o.except); + const explore = !ignore && matches(node, o.only); + + if (explore) { + let transformedNode = node; + for (const transformation of transformations) { + const callback = typeof transformation === "object" ? transformation[transformedNode.type] : transformation; + transformedNode = callback?.(transformedNode, property, parent, node); + + if (transformedNode === undefined) { + transformedNode = node; + } } + node = transformedNode; - return ret; + set(node, parent, {force: true}); + const properties$1 = properties[node.type] ?? []; + for (const prop of properties$1) { + node[prop] = _transform(node[prop], transformations, o, prop, node); + } } + + return node; + } + + /** + * Recursively execute a callback on this node and all its children. + * If the callback returns a non-undefined value, it will overwrite the node, + * otherwise it will return a shallow clone. + * @param {object | object[]} node AST node or array of nodes + * @param {Object. | function(object, string, object?) | (Object. | function(object, string, object?))[]} mappings A map of node types to callbacks, or a single callback that will be called for all node types, or a list of either, which will be applied in order + * @param {object} [o] + * @param {string | string[] | function} [o.only] Only walk nodes of this type + * @param {string | string[] | function} [o.except] Ignore walking nodes of these types + */ + function map (node, mappings, o) { + const cloneFn = (node, property, parent, originalNode) => node === originalNode ? {...node} : node; + mappings = [mappings, cloneFn].flat(); + return transform(node, mappings, o); } /** @@ -152,7 +276,10 @@ var Vastly = (function (exports) { "UnaryExpression": (node, ...contexts) => { let operator = unaryOperators[node.operator]; if (!operator) { - throw new TypeError(`Unknown unary operator ${node.operator}`); + throw new TypeError(`Unknown unary operator ${node.operator}`, { + code: "UNKNOWN_UNARY_OPERATOR", + node, + }); } return operator(evaluate(node.argument, ...contexts)); @@ -161,7 +288,10 @@ var Vastly = (function (exports) { "BinaryExpression": (node, ...contexts) => { let operator = binaryOperators[node.operator]; if (!operator) { - throw new TypeError(`Unknown binary operator ${node.operator}`); + throw new TypeError(`Unknown binary operator ${node.operator}`, { + code: "UNKNOWN_BINARY_OPERATOR", + node, + }); } return operator(evaluate(node.left, ...contexts), evaluate(node.right, ...contexts)); @@ -176,7 +306,11 @@ var Vastly = (function (exports) { let property = node.computed ? evaluate(node.property, ...contexts) : node.property.name; if (!object) { - throw new TypeError(`Cannot read properties of ${object} (reading '${property}')`); + throw new TypeError(`Cannot read properties of ${object} (reading '${property}')`, { + code: "PROPERTY_REF_EMPTY_OBJECT", + node, + contexts, + }); } return object[property]; @@ -206,45 +340,20 @@ var Vastly = (function (exports) { return evaluators[node.type](node, ...contexts); } - throw new TypeError(`Cannot evaluate node of type ${node.type}`); + throw new TypeError(`Cannot evaluate node of type ${node.type}`, { + code: "UNKNOWN_NODE_TYPE", + node, + }); } + evaluate.evaluators = evaluators; + function resolve (property, ...contexts) { let context = contexts.find(context => property in context); return context?.[property]; } - /** - * Recursively execute a callback on this node and all its children. - * If the callback returns a non-undefined value, walking ends and the value is returned - * @param {object} node - * @param {function(object, string, object?)} callback - * @param {object} [o] - * @param {string | string[] | function} [o.only] Only walk nodes of this type - * @param {string | string[] | function} [o.except] Ignore walking nodes of these types - */ - function walk (node, callback, o) { - return _walk(node, callback, o); - } - - function _walk(node, callback, o = {}, property, parent) { - let ignored = o.except && matches(node, o.except); - - if (!ignored && matches(node, o.only)) { - let ret = callback(node, property, parent); - - if (ret !== undefined) { - // Callback returned a value, stop walking and return it - return ret; - } - - for (let child of children(node)) { - _walk(child, callback, o, property, node); - } - } - } - /** * Find an AST node and return it, or `null` if not found. * @param {object | object[]} node @@ -274,7 +383,7 @@ var Vastly = (function (exports) { }, "ConditionalExpression": node => `${serialize(node.test)} ? ${serialize(node.consequent)} : ${serialize(node.alternate)}`, "MemberExpression": node => { - let property = node.computed? `[${serialize(node.property)}]` : `.${node.property.name}`; + let property = node.computed ? `[${serialize(node.property)}]` : `.${node.property.name}`; let object = serialize(node.object); return `${object}${property}`; }, @@ -285,50 +394,48 @@ var Vastly = (function (exports) { "Compound": node => node.body.map(n => serialize(n)).join(", ") }; - /** - * Transformations to apply to the AST before serializing, by node type. - * @type {Object.} - */ - const transformations = {}; - /** * Recursively serialize an AST node into a JS expression * @param {*} node - * @returns + * @returns {string} Serialized expression */ function serialize (node) { if (!node || typeof node === "string") { return node; // already serialized } - let ret = transformations[node.type]?.(node) ?? node; - - if (typeof ret == "object" && ret?.type) { - node = ret; - } - else if (ret !== undefined) { - return ret; + if (!node.type) { + throw new TypeError(`AST node ${node} has no type`, { + cause: { + code: "NODE_MISSING_TYPE", + node, + } + }); } - if (!node.type || !serializers[node.type]) { - throw new TypeError("Cannot understand this expression at all 😔"); + if (!serializers[node.type]) { + throw new TypeError(`No serializer found for AST node with type '${ node.type }'`, { + cause: { + code: "UNKNOWN_NODE_TYPE", + node, + } + }); } return serializers[node.type](node); } serialize.serializers = serializers; - serialize.transformations = transformations; /** * Recursively traverse the AST and return all top-level identifiers * @param {object} node the AST node to traverse * @returns a list of all top-level identifiers */ - function variables(node) { - switch(node.type) { + function variables (node) { + switch (node.type) { case "Literal": - return [] + return []; case "UnaryExpression": return variables(node.argument); case "BinaryExpression": @@ -360,54 +467,6 @@ var Vastly = (function (exports) { } } - /** - * Set properties on each node pointing to its parent node. - * Required for many vastly functions, e.g. `closest()`. - * By default it will skip nodes that already have a `parent` property entirely, but you can set force = true to prevent that. - * @param {*} node - * @param {object} [options] - * @param {boolean} [options.force] Overwrite existing `parent` properties - */ - function setAll (node, options ) { - walk(node, (node, property, parent) => { - let ret = set(node, parent, options); - - if (ret === false) { - // We assume that if the node already has a parent, its subtree will also have parents - return false; - } - }); - } - - /** - * Set the `parent` property on a node. - * By default it will skip nodes that already have a `parent` property, but you can set force = true to prevent that. - * @param {object} node - * @param {object} parent - * @param {object} [options] - * @param {boolean} [options.force] Allow overwriting - */ - function set (node, parent, { force } = {}) { - if (!force && "parent" in node) { - // We assume that if the node already has a parent, its subtree will also have parents - return false; - } - else { - Object.defineProperty(node, "parent", { - value: parent, - enumerable: false, - configurable: true, - writable: true - }); - } - } - - var parents = /*#__PURE__*/Object.freeze({ - __proto__: null, - setAll: setAll, - set: set - }); - exports.children = children; exports.closest = closest; exports.evaluate = evaluate; diff --git a/src/mavoscript.js b/src/mavoscript.js index e4116ce3..7667c0b7 100644 --- a/src/mavoscript.js +++ b/src/mavoscript.js @@ -561,8 +561,8 @@ var _ = Mavo.Script = { * These serializers transform the AST into JS */ serializers: { - "BinaryExpression": node => `${_.serialize(node.left, node)} ${node.operator} ${_.serialize(node.right, node)}`, - "UnaryExpression": node => `${node.operator}${_.serialize(node.argument, node)}`, + "BinaryExpression": node => `${_._serialize(node.left, node)} ${node.operator} ${_._serialize(node.right, node)}`, + "UnaryExpression": node => `${node.operator}${_._serialize(node.argument, node)}`, "CallExpression": node => { var callee = node.callee; let root = node.callee; @@ -599,11 +599,11 @@ var _ = Mavo.Script = { } } - var nameSerialized = _.serialize(node.callee, node); - var argsSerialized = node.arguments.map(n => _.serialize(n, node)); + var nameSerialized = _._serialize(node.callee, node); + var argsSerialized = node.arguments.map(n => _._serialize(n, node)); return `${nameSerialized}(${argsSerialized.join(", ")})`; }, - "ConditionalExpression": node => `${_.serialize(node.test, node)}? ${_.serialize(node.consequent, node)} : ${_.serialize(node.alternate, node)}`, + "ConditionalExpression": node => `${_._serialize(node.test, node)}? ${_._serialize(node.consequent, node)} : ${_._serialize(node.alternate, node)}`, "MemberExpression": (node) => { let n = node, pn, callee; @@ -615,23 +615,23 @@ var _ = Mavo.Script = { } while (n = n.parent); if (n) { // Use plain serialization for foo.bar.baz() - var property = node.computed? `[${_.serialize(node.property, node)}]` : `.${node.property.name}`; - return `${_.serialize(node.object, node)}${property}`; + var property = node.computed? `[${_._serialize(node.property, node)}]` : `.${node.property.name}`; + return `${_._serialize(node.object, node)}${property}`; } n = node; let properties = [], object, objectParent; while (n.type === "MemberExpression") { - let serialized = n.computed? _.serialize(n.property, n) : `"${n.property.name}"`; + let serialized = n.computed? _._serialize(n.property, n) : `"${n.property.name}"`; properties.push(serialized); objectParent = n; object = n = n.object; } - return `$fn.get(${_.serialize(object, objectParent)}, ${properties.reverse().join(", ")})`; + return `$fn.get(${_._serialize(object, objectParent)}, ${properties.reverse().join(", ")})`; }, - "ArrayExpression": node => `[${node.elements.map(n => _.serialize(n, node)).join(", ")}]`, + "ArrayExpression": node => `[${node.elements.map(n => _._serialize(n, node)).join(", ")}]`, "Literal": node => { let quote = node.raw[0]; @@ -648,7 +648,7 @@ var _ = Mavo.Script = { }, "Identifier": node => node.name, "ThisExpression": node => "this", - "Compound": node => node.body.map(n => _.serialize(n, node)).join(", ") + "Compound": node => node.body.map(n => _._serialize(n, node)).join(", ") }, /** @@ -797,13 +797,18 @@ var _ = Mavo.Script = { closest: Vastly.closest, - serialize: (node, parent) => { + _serialize: (node, parent) => { if (parent) { Vastly.parents.set(node, parent); } return Vastly.serialize(node); }, + serialize: (node) => { + node = Vastly.map(node, _.transformations); + return _._serialize(node); + }, + rewrite: function(code, o) { let ast = _.parse(code); @@ -848,9 +853,9 @@ Mavo.Actions.running = Mavo.Actions._running;`; // scope() rewriting serializeScopeCall: (args) => { - var withCode = `with (Mavo.Script.subScope(scope, $this) || {}) { return (${_.serialize(args[1])}); }`; + var withCode = `with (Mavo.Script.subScope(scope, $this) || {}) { return (${_._serialize(args[1])}); }`; return `(function() { - var scope = ${_.serialize(args[0])}; + var scope = ${_._serialize(args[0])}; if (Array.isArray(scope)) { return scope.map(function(scope) { ${withCode} @@ -887,7 +892,6 @@ Mavo.Actions.running = Mavo.Actions._running;`; _.serializers.LogicalExpression = _.serializers.BinaryExpression; _.transformations.LogicalExpression = _.transformations.BinaryExpression; -Object.assign(Vastly.serialize.transformations, _.transformations); Object.assign(Vastly.serialize.serializers, _.serializers); for (let name in _.operators) {