UNPKG

ion

Version:

ion language ========================

1,277 lines (1,173 loc) 52.8 kB
{traverse} = require './traverseAst' basicTraverse = require('./traverse').traverse {addStatement,forEachDestructuringAssignment} = require './astFunctions' nodes = require './nodes' ion = require '../' undefinedExpression = Object.freeze type: 'UnaryExpression' argument: type: 'Literal' value: 0 operator: 'void' prefix: true nullExpression = Object.freeze type: 'Literal' value: null trueExpression = Object.freeze type: 'Literal' value: true falseExpression = Object.freeze type: 'Literal' value: false ionExpression = Object.freeze type: 'Identifier' name: 'ion' thisExpression = Object.freeze type: 'ThisExpression' isPattern = (node) -> node.properties? or node.elements? isVariableReference = (node, parent, key) -> return node.type is 'Identifier' and (not parent? or not (parent.type is 'MemberExpression' and key is 'property' or parent.type is 'Property' and key is 'key')) isObjectPatch = (node) -> node? and node.type is 'ObjectExpression' and (node.objectType? or node.create?) isConstantLiteral = (node, parent, key) -> if isObjectPatch(node) return false if not node? return false if node.type is 'Literal' return true if isVariableReference(node, parent, key) return false if node.type is 'ObjectExpression' for {type,value,computed}, index in node.properties if computed or type isnt 'Property' or not isConstantLiteral(value, node, index) return false return true else if node.type is 'ArrayExpression' for value, index in node.elements if not isConstantLiteral(value, node, index) return false return true else return false nodeToLiteral = (object, checkForLiteral) -> if checkForLiteral isnt false and isObjectPatch(object) checkForLiteral = false node = null if object?.toLiteral? node = object?.toLiteral() # else if checkForLiteral isnt false and object? and (object.type is 'ArrayExpression' or object.type is 'ObjectExpression') and isConstantLiteral(object) # node = # type: 'ObjectExpression' # properties: [ # {type:'Property',key:{type:"Identifier",name:"type"},value:{type:"Literal",value:"Literal"}} # {type:'Property',key:{type:"Identifier",name:"value"},value:object} # ] else if Array.isArray object node = type: 'ArrayExpression' elements: (nodeToLiteral(item, checkForLiteral) for item in object) else if object?.constructor is Object node = type: 'ObjectExpression' properties: [] for key, value of object if value isnt undefined node.properties.push key: type: 'Identifier' name: key value: nodeToLiteral(value, checkForLiteral) kind: 'init' else node = type: 'Literal' value: object return node getPathExpression = (path) -> steps = path.split '.' if steps[0] is 'this' result = type: 'ThisExpression' else result = type: 'Identifier' name: steps[0] for step, i in steps when i > 0 result = type: 'MemberExpression' object: result property: type: 'Identifier' name: step return result isFunctionNode = (node) -> nodes[node?.type]?.isFunction ? false # wraps a node in a BlockStatement if it isn't already. block = (node) -> if node.type isnt 'BlockStatement' node = type: 'BlockStatement' body: [node] return node extractReactiveForPatterns = (node, context) -> return if not context.reactive if node.type is 'ForOfStatement' or node.type is 'ForInStatement' for declarator, index in node.left.declarations when isPattern declarator.id ref = context.getNewInternalIdentifier() # extract the object pattern into a destructuring assignment from a new variable context.addStatement type: 'VariableDeclaration' declarations: [ type: 'VariableDeclarator' id: declarator.id init: ref ] # replace it with a reference to the new variable declarator.id = ref extractForLoopRightVariable = (node, context) -> return if context.reactive if node.type is 'ForOfStatement' or (node.type is 'ForInStatement' and node.left.declarations.length > 1) if node.left.declarations.length > 2 throw context.error "too many declarations", node.left.declarations[2] right = node.right if right.type isnt "Identifier" ref = context.getNewInternalIdentifier() node.right = ref context.replace type: "BlockStatement" body: [ {type:"VariableDeclaration",declarations:[{type:"VariableDeclarator",id:ref,init:right}],kind:node.left.kind} node ] createForInLoopValueVariable = (node, context) -> return if context.reactive if node.type is 'ForInStatement' and node.left.declarations.length > 1 valueDeclarator = node.left.declarations[1] # TODO: Come up with a better fix for this. # Find out why it's being visited more than once. # It's probably because of all the tree manipulation. # I imagine an insertion may be shifting the elements # ahead to be retraversed while traversing an array. # -Kody if node.visited_createForInLoopValueVariable return node.visited_createForInLoopValueVariable = true context.addVariable id: valueDeclarator.id init: type: 'MemberExpression' computed: true object: node.right property: node.left.declarations[0].id convertForInToForLength = (node, context) -> return if context.reactive if node.type is 'ForOfStatement' userIndex = node.left.declarations[1]?.id loopIndex = context.getNewInternalIdentifier "_i" addStatement node, type:"VariableDeclaration" declarations:[ { type:"VariableDeclarator" id: node.left.declarations[0].id init: type: "MemberExpression" object: node.right property: loopIndex computed: true } ] kind: node.left.kind if userIndex? addStatement node, type:"VariableDeclaration" declarations:[ { type:"VariableDeclarator" id: userIndex init: loopIndex } ] kind: node.left.kind context.replace type: 'ForStatement' init: type:"VariableDeclaration" declarations:[ { type:"VariableDeclarator",id:loopIndex,init:{ type:"Literal", value:0 } } ] kind: 'let' test: type: "BinaryExpression" operator: "<" left: loopIndex right: type: "MemberExpression" object: node.right property: { type: "Identifier", name: "length" } computed: false update: type: "UpdateExpression" operator: "++" argument: loopIndex prefix: false body: node.body callFunctionBindForFatArrows = (node, context) -> if node.type is 'FunctionExpression' and node.bound delete node.bound ensureIonVariable context context.replace type: "CallExpression" callee: getPathExpression 'ion.bind' arguments: [ node, thisExpression ] nodejsModules = (node, context) -> # convert ImportExpression{name} into require(name) if node.type is 'ImportExpression' node.type = 'CallExpression' node.callee = type: 'Identifier' name: 'require' node.arguments = [node.name] delete node.name else if node.type is 'ExportStatement' if node.value.type is 'VariableDeclaration' # variable export context.exports = true # replace this node with the VariableDeclaration context.replace node.value # then make each init also assign to it's export variable. for declarator in node.value.declarations by -1 if not declarator.init? throw context.error "Export variables must have an init value", declarator declarator.init = type: 'AssignmentExpression' operator: '=' left: type: 'MemberExpression' object: type: 'Identifier' name: 'exports' property: declarator.id right: declarator.init else # default export if context.exports throw context.error "default export must be first" context.replace type: 'ExpressionStatement' expression: type: 'AssignmentExpression' operator: '=' left: type: 'MemberExpression' object: type: 'Identifier' name: 'module' property: type: 'Identifier' name: 'exports' right: type: 'AssignmentExpression' operator: '=' left: type: 'Identifier' name: 'exports' right: node.value # separateAllVariableDeclarations = (node, context) -> # if node.type is 'VariableDeclaration' and context.isParentBlock() # while node.declarations.length > 1 # declaration = node.declarations.pop() # context.addStatement # type: node.type # declarations: [declaration] # kind: node.kind destructuringAssignments = (node, context) -> # function parameters if isFunctionNode node for pattern, index in node.params when isPattern pattern tempId = context.getNewInternalIdentifier() node.params[index] = tempId statements = [] forEachDestructuringAssignment pattern, tempId, (id, expression) -> statements.unshift { type: 'VariableDeclaration' declarations: [{ type: 'VariableDeclarator' id: id init: expression }] kind: 'let' } for statement in statements context.addStatement statement # variable assignments if node.type is 'VariableDeclaration' and context.isParentBlock() count = 0 for declarator, declaratorIndex in node.declarations when isPattern declarator.id pattern = declarator.id tempId = context.getNewInternalIdentifier() declarator.id = tempId # we must extract the init and add it later, otherwise... # it may reference a value which hasn't been destructured yet from a previous declarator if declarator.init? and declaratorIndex > 0 context.addStatement { type: 'ExpressionStatement' expression: type: 'AssignmentExpression' operator: '=' left: tempId right: declarator.init }, ++count declarator.init = null node.kind = 'let' forEachDestructuringAssignment pattern, tempId, (id, expression) -> context.addStatement { type: 'VariableDeclaration' declarations: [{ type: 'VariableDeclarator' id: id init: expression }] kind: 'let' }, ++count # other assignments if node.type is 'ExpressionStatement' and node.expression.operator is '=' expression = node.expression pattern = expression.left if isPattern pattern tempId = context.getNewInternalIdentifier() context.replace type: 'VariableDeclaration' declarations: [{ type: 'VariableDeclarator' id: tempId init: expression.right }] kind: 'const' count = 0 forEachDestructuringAssignment pattern, tempId, (id, expression) -> context.addStatement { type: 'ExpressionStatement' expression: type: 'AssignmentExpression' operator: '=' left: id right: expression }, ++count defaultOperatorsToConditionals = (node, context) -> if node.type is 'BinaryExpression' and (node.operator is '??' or node.operator is '?') context.replace type: 'ConditionalExpression' test: type: 'BinaryExpression' operator: '!=' left: node.left right: if node.operator is '??' then undefinedExpression else nullExpression consequent: node.left alternate: node.right defaultAssignmentsToDefaultOperators = (node, context) -> if node.type is 'AssignmentExpression' and (node.operator is '?=' or node.operator is '??=') # a ?= b --> a = a ? b node.right = type: 'BinaryExpression' operator: if node.operator is '?=' then '?' else '??' left: node.left right: node.right node.operator = '=' existentialExpression = (node, context) -> if node.type is 'UnaryExpression' and node.operator is '?' context.replace type: 'BinaryExpression' operator: '!=' left: node.argument right: nullExpression # this could be more efficient by caching the left values # especially when the left side involves existential CallExpressions # should only apply within an imperative context if node.type is 'MemberExpression' or node.type is 'CallExpression' and not context.reactive # search descendant objects for deepest existential child getExistentialDescendantObject = (check) -> result = null if check.type is 'MemberExpression' or check.type is 'CallExpression' result = getExistentialDescendantObject check.object ? check.callee if check.existential result ?= check return result # create temp ref variable # a?.b --> a != null ? a.b : undefined existentialChild = getExistentialDescendantObject node if existentialChild? existentialChildObject = existentialChild.object ? existentialChild.callee delete existentialChild.existential context.replace type: 'ConditionalExpression' test: type: 'BinaryExpression' operator: '!=' left: existentialChildObject right: nullExpression consequent: node alternate: undefinedExpression ensureIonVariable = (context, required = true) -> context.ancestorNodes[0].requiresIon = required addUseStrictAndRequireIon = enter: (node, context) -> # see if we are already importing ion at the Program scope if node.type is 'VariableDeclaration' and context.parentNode()?.type is 'Program' for d in node.declarations when d.id.name is 'ion' # we don't need to import ion because the user already did ensureIonVariable context, false break exit: (node, context) -> if node.type is 'Program' if node.requiresIon delete node.requiresIon context.addVariable offset: Number.MIN_VALUE kind: 'const' id: ionExpression init: type: 'ImportExpression' name: type: 'Literal' value: 'ion' node.body.unshift type: 'ExpressionStatement' expression: type: 'Literal' value: 'use strict' extractForLoopsInnerAndTest = (node, context) -> if node.type is 'ForInStatement' or node.type is 'ForOfStatement' if node.inner? node.inner.body = node.body node.body = node.inner delete node.inner if node.test? node.body = block type: 'IfStatement' test: node.test consequent: block node.body delete node.test arrayComprehensionsToES5 = (node, context) -> if node.type is 'ArrayExpression' and node.value? and node.comprehension? if context.reactive # convert it to a typed object expression forStatement = node.comprehension forStatement.body = type: 'ExpressionStatement' expression: node.value context.replace type: 'ObjectExpression' objectType: type: 'ArrayExpression' elements: [] properties: [forStatement] else # add a statement tempId = context.addVariable offset: 0 init: type: 'ArrayExpression' elements: [] forStatement = node.comprehension forStatement.body = type: 'ExpressionStatement' expression: type: 'CallExpression' callee: type: 'MemberExpression' object: tempId property: type: 'Identifier' name: 'push' arguments: [node.value] context.addStatement 0, forStatement context.replace tempId functionParameterDefaultValuesToES5 = (node, context) -> return if context.reactive if isFunctionNode(node) and node.params? and node.defaults? for param, index in node.params by -1 defaultValue = node.defaults?[index] if defaultValue? context.addStatement type: 'IfStatement' test: type: 'BinaryExpression' operator: '==' left: param right: nullExpression consequent: type: 'ExpressionStatement' expression: type: 'AssignmentExpression' operator: '=' left: param right: defaultValue node.defaults[index] = undefined isSimpleObjectExpression = (node) -> if node.type isnt 'ObjectExpression' return false isArray = node.objectType?.type is "ArrayExpression" isSimple = true if node.properties? for property in node.properties if isArray if property.type isnt 'ExpressionStatement' isSimple = false break else if property.type isnt 'Property' or property.computed isSimple = false break return isSimple typedObjectExpressions = (node, context) -> # only for imperative code return if context.reactive if node.type is 'ObjectExpression' and node.simple isnt true isArray = node.objectType?.type is "ArrayExpression" isSimple = isSimpleObjectExpression node # empty object expression without properties {} if isSimple if isArray elements = [] if node.objectType? for element in node.objectType.elements elements.push element for expressionStatement in node.properties elements.push expressionStatement.expression context.replace type: "ArrayExpression" elements: elements return if (not node.objectType? or (node.objectType.type is 'ObjectExpression' and node.objectType.properties.length is 0)) # check that our properties ONLY contain normal Property objects with no computed values delete node.objectType # set simple to true, but make it non-enumerable so we don't write it out Object.defineProperty node, 'simple', {value:true} return # convert this simple expression to an ion.patch call objectType = node.objectType delete node.objectType # if the objectType is simple as well, then merge them if isSimpleObjectExpression objectType node.properties = objectType.properties.concat(node.properties) Object.defineProperty node, 'simple', {value:true} return else ensureIonVariable context context.replace type: 'CallExpression' callee: getPathExpression 'ion.patch.combine' arguments: [objectType, node] return if not node.objectType? initialValue = type: 'ObjectExpression' properties: [] else initialValue = node.objectType parentNode = context.parentNode() grandNode = context.ancestorNodes[context.ancestorNodes.length-2] addPosition = 0 getExistingObjectIdIfTempVarNotNeeded = (node, parentNode, grandNode) -> # don't need a temp variable because nothing can trigger on variable declaration if parentNode.type is 'VariableDeclarator' return parentNode.id # don't need a temp variable because nothing can trigger on variable assignment if parentNode.type is 'AssignmentExpression' and parentNode.left.type is 'Identifier' and grandNode?.type is 'ExpressionStatement' return parentNode.left # for everything else we must use a temp variable and assign all sub properties # before using the final value in an expression, because it may trigger a setter # or be a parameter in a function call or constructor return null objectId = getExistingObjectIdIfTempVarNotNeeded node, parentNode, grandNode if objectId? # replace this with the initial value context.replace initialValue addPosition = 1 else # create a temp variable objectId = context.addVariable offset: 0 init: initialValue # replace this with a reference to the variable context.replace objectId statements = [] setNodeOutputValues context, node.properties, objectId, statements, isArray if statements.length is 1 context.addStatement statements[0], addPosition else context.addStatement {type:'BlockStatement',body:statements}, addPosition setNodeOutputValues = (context, nodes, outputId, statements = [], isArray) -> # traverse all properties and expression statements # add a new property that indicates their output scope subnodeEnter = (subnode, subcontext) -> subcontext.outputStack ?= [outputId] if subnode.type is 'ObjectExpression' or subnode.type is 'ArrayExpression' return subcontext.skip() if subnode.type is 'Property' #or subnode.type is 'ExpressionStatement' # we convert the node to a Property: ObjectExpression node # it will be handled correctly by the later propertyStatements rule subnode.output = subcontext.outputStack[subcontext.outputStack.length - 1] subcontext.outputStack.push type: 'MemberExpression' object: subnode.output property: subnode.key computed: subnode.computed || subnode.key.type isnt 'Identifier' else if isFunctionNode(subnode) subcontext.skip() else if subnode.type is 'ExpressionStatement' if not isArray ensureIonVariable(context) subnode = subcontext.replace type: 'ExpressionStatement' expression: type: 'CallExpression' callee: type: 'MemberExpression' object: if isArray then outputId else ionExpression property: type: 'Identifier' name: if isArray then 'push' else 'add' arguments: if isArray then [subnode.expression] else [outputId, subnode.expression] subcontext.skip() if not subcontext.parentNode()? # add this statement to the current context statements.push subnode subnodeExit = (subnode, subcontext) -> if subnode.type is 'Property' subcontext.outputStack.pop() traverse nodes, subnodeEnter, subnodeExit return statements propertyStatements = (node, context) -> return if context.reactive parent = context.parentNode() if node.type is 'Property' and not (parent.type is 'ObjectExpression' or parent.type is 'ObjectPattern') if node.output? if node.value.type is 'ObjectExpression' left = type: 'MemberExpression' object: node.output property: node.key computed: node.computed if node.value.type is 'ObjectExpression' and not node.value.objectType? ensureIonVariable(context) right = type: 'CallExpression' callee: getPathExpression 'ion.patch.combine' arguments: [ion.clone(left, true), node.value] else right = node.value context.replace type: 'ExpressionStatement' expression: type: 'AssignmentExpression' operator: '=' left: left right: right else context.replace type: 'ExpressionStatement' expression: type: 'AssignmentExpression' operator: '=' left: type: 'MemberExpression' object: node.output property: node.key computed: node.computed right: node.value else throw context.error "Property assignment only valid within an object declaration", node patchAssignmentExpression = (node, context) -> if node.type is 'AssignmentExpression' and node.operator is ':=' ensureIonVariable context context.replace type: 'AssignmentExpression' operator: '=' left: node.left right: type: 'CallExpression' callee: getPathExpression 'ion.patch.combine' arguments: [ion.clone(node.left, true), node.right] classExpressions = (node, context) -> if node.type is 'ClassExpression' ensureIonVariable context properties = node.properties hasIdentifierName = node.name? and not node.computed if node.name? name = if hasIdentifierName then {type:'Literal',value:node.name.name} else node.name # add name to the properties properties = [{type:'Property',key:{type:'Identifier',name:'name'},value:name}].concat properties # set the class name on the constructor function if hasIdentifierName for property in properties when property.key.name is 'constructor' property.value.id ?= node.name classExpression = type: 'CallExpression' callee: type: 'MemberExpression' object: ionExpression property: type: 'Identifier' name: 'defineClass' arguments: [{type: 'ObjectExpression',properties: properties}].concat node.extends if hasIdentifierName context.addVariable id: node.name kind: 'const' init: classExpression offset: 0 context.replace node.name else context.replace classExpression checkVariableDeclarations = enter: (node, context) -> # check assigning to a constant if node.type is 'AssignmentExpression' if node.left.type is 'Identifier' variable = context.getVariableInfo(node.left.name) if not variable? throw context.error "cannot assign to undeclared variable #{node.left.name}" if variable.kind is 'const' throw context.error "cannot assign to a const", node.left if context.reactive throw context.error "cannot assign within templates", node # track variable usage on a scope if isVariableReference(node, context.parentNode(), context.key()) # then this is a variable usage, so we will track it. (context.scope().usage ?= {})[node.name] = node variable: (variable, context) -> scope = context.scope() # check that we arent redeclaring a variable existing = context.getVariableInfo(variable.name) if existing? # check to see if shadowing is allowed. # walk the scope stack backwards shadow = false for checkScope in context.scopeStack by -1 # we only check back until we hit the scope # where the existing variable was declared if checkScope is existing?.scope break # if we pass a scope that allows shadowing then we are ok if nodes[checkScope.node.type]?.shadow shadow = true break if not shadow throw context.error "Cannot redeclare variable #{variable.name}", variable.node # make sure we havent used this variable before declaration for checkScope in context.scopeStack by -1 used = checkScope.usage?[variable.name] if used? throw context.error "Cannot use variable '#{variable.name}' before declaration", used # we only check back to a shadow, max if nodes[checkScope.node.type]?.shadow break isAncestorObjectExpression = (context) -> for ancestor in context.ancestorNodes by -1 if ancestor.type is 'ObjectExpression' return true if isFunctionNode(ancestor) return false return false namedFunctionsAndNewArguments = (node, context) -> return if context.reactive if node.type is 'NewExpression' node.arguments ?= [] # these names are used later by the classExpression rule # add an internal name to functions declared as variables if node.type is 'VariableDeclarator' and node.init?.type is 'FunctionExpression' node.init.name ?= node.id # add an internal name to functions declared as properties if node.type is 'Property' and node.value.type is 'FunctionExpression' and node.key.type is 'Identifier' if node.key.name isnt 'constructor' node.value.name ?= node.key assertStatements = (node, context) -> if node.type is 'AssertStatement' context.replace type: 'IfStatement' test: type: 'UnaryExpression' prefix: true operator: '!' argument: node.expression consequent: type: 'ThrowStatement' argument: type: 'NewExpression' callee: type: 'Identifier' name: 'Error' arguments: [ type: 'Literal' value: "Assertion Failed: (#{node.text})" ] isSuperExpression = (node, context) -> parentNode = context.parentNode() if node.type is 'Identifier' and node.name is 'super' and parentNode.type isnt 'CallExpression' and parentNode.type isnt 'MemberExpression' return true if node.type is 'CallExpression' and node.callee.type is 'Identifier' and node.callee.name is 'super' return true return false superExpressions = (node, context) -> if isSuperExpression node, context classNode = context.getAncestor (node) -> node.type is 'ClassExpression' functionNode = context.getAncestor isFunctionNode functionProperty = context.ancestorNodes[context.ancestorNodes.indexOf(functionNode) - 1] isConstructor = functionProperty?.key?.name is 'constructor' if not classNode? or not (functionNode?.name? or isConstructor) throw context.error "super can only be used within named class functions", node args = [{type:'ThisExpression'}] if node.type is 'Identifier' args.push {type:'Identifier',name:'arguments'} applyOrCall = 'apply' else args = args.concat node.arguments applyOrCall = 'call' superFunction = getPathExpression "#{classNode.name.name}.super" if not isConstructor superFunction = type: 'MemberExpression' object: type: 'MemberExpression' object: superFunction property: type: 'Identifier' name: 'prototype' property: functionNode.name ? 'constructor' context.replace type: 'CallExpression' callee: type: 'MemberExpression' object: superFunction property: type: 'Identifier' name: applyOrCall arguments: args spreadExpressions = (node, context) -> # function rest parameters if isFunctionNode node spread = null spreadIndex = null for param, index in node.params if param.type is 'SpreadExpression' spread = param spreadIndex = index break if spread? # replace the spread parameter with a placeholder named parameter node.params[spreadIndex] = type: 'Identifier' name: "___" + spread.expression.name # add a variable that extracts the spread args = [ {type:'Identifier', name:'arguments'} {type:'Literal', value:spreadIndex} ] finalParameters = node.params.length - 1 - spreadIndex if finalParameters > 0 # add a third arg to the slice that removes the final parameters from the end getOffsetFromArgumentsLength = (offset) -> return { type: 'BinaryExpression' operator: '-' left: getPathExpression 'arguments.length' right: {type:'Literal', value: offset } } args.push getOffsetFromArgumentsLength finalParameters # extract the correct values for the final variables. index = node.params.length - 1 while index > spreadIndex param = node.params[index--] context.addStatement type: 'ExpressionStatement' expression: type: 'AssignmentExpression' operator: '=' left: param right: type: 'MemberExpression' computed: true object: getPathExpression 'arguments' property: getOffsetFromArgumentsLength node.params.length - 1 - index context.addVariable id: spread.expression init: type: 'CallExpression' callee: getPathExpression 'Array.prototype.slice.call' arguments: args validateTemplateNodes = (node, context) -> if context.reactive if nodes[node.type]?.allowedInReactive is false throw context.error node.type + " not allowed in templates", node removeLocationInfo = (node) -> traverse node, (node) -> if node.loc? and not nodes[node?.type]?.location delete node.loc return node isReferenceNode = (node, context) -> if node.type isnt 'Identifier' return false parentNode = context.parentNode() # ignore function names if isFunctionNode(parentNode) return false # ignore variable declarations if parentNode?.type is 'VariableDeclarator' and context.key() is 'id' return false # ignore member expression right hand identifiers if parentNode?.type is 'MemberExpression' and not parentNode?.computed and context.key() is 'property' return false # ignore object property keys if parentNode?.type is 'Property' and context.key() is 'key' return false return true # gets all identifiers, except member access properties getReferenceIdentifiers = (node, callback) -> results = {} callback ?= (node) -> results[node.name] = node traverse node, (node, context) -> isRef = isReferenceNode(node, context) if isRef callback(node, context) return results # gets all identifiers, except member access properties getExternalIdentifiers = (node, callback) -> getReferenceIdentifiers node, (node, context) -> # ignore internally defined variables if context.getVariableInfo(node.name)? return callback(node, context) return wrapTemplateInnerFunctions = (node, context) -> if context.parentReactive() if node.type is 'FunctionExpression' and not node.toLiteral? # see if we need to replace any properties in this function or not. variables = {} getExternalIdentifiers node, (id) -> if id.name is 'ion' or (id.name isnt node.id?.name and context.scope()?.variables[id.name]?) variables[id.name] = id requiresWrapper = Object.keys(variables).length > 0 if requiresWrapper # now convert the node to a new wrapped node # add a statement extracting each needed variable from the reactive context contextId = context.getNewInternalIdentifier('_context') node.body.body.unshift type: 'VariableDeclaration' kind: 'const' declarations: (for name, id of variables type: 'VariableDeclarator' id: id init: type: 'CallExpression' callee: getPathExpression "#{contextId.name}.get" arguments: [ type: 'Literal' value: id.name ] ) node = type: 'FunctionExpression' params: [contextId] body: type: 'BlockStatement' body: [ type: 'ReturnStatement' argument: node ] node.toLiteral = -> @ context.replace type: 'Function' context: requiresWrapper value: node createTemplateFunctionClone = (node, context) -> if isFunctionNode(node) and node.template is true delete node.template node.type = 'Template' ensureIonVariable context newNode = type: 'CallExpression' callee: getPathExpression 'ion.template' arguments: [node] toLiteral: -> @ # if this is an inner template we must wrap it to get the parents scoped variables. inner = context.parentReactive() if inner scope = context.getNewInternalIdentifier('_ps') node.scope = scope # replace the node with a function that will be called with context newNode = type: 'Function' context: true value: type: 'FunctionExpression' params: [scope] toLiteral: -> @ body: block( type: 'ReturnStatement' argument: newNode ) context.replace newNode createTemplateRuntime = (node, context) -> if node.type is 'Template' template = removeLocationInfo node templateId = node.id # create an arguments object that contains all the parameter values. args = type: 'ObjectExpression' properties: [] variables = this: thisExpression # get all reference identifiers referenceIds = getReferenceIdentifiers(node) # if nodejs, add built in ids (but only if we use them) for name in ['require', 'module', 'exports', 'ion'] if referenceIds[name]? variables[name] = { type: 'Identifier', name: name } for id in template.params variables[id.name] = id # also add any variables in scope for key, value of context.scope().variables id = value.id variables[id.name] = id for key, id of variables args.properties.push key: id value: id kind: 'init' params = template.params # remove the extra blockStatement. template.body = template.body.body # delete template.id delete template.params delete template.defaults newNode = type: 'FunctionExpression' params: params body: type: 'BlockStatement' body: [ type: 'ReturnStatement' argument: type: 'CallExpression' callee: getPathExpression 'ion.createRuntime' arguments: [ nodeToLiteral template args node.scope ? nullExpression ] ] if templateId? newNode.id = templateId context.replace(newNode) javascriptExpressions = (node, context) -> if node.type is 'JavascriptExpression' try esprima = require 'esprima' catch e # no esprima, so lets do a raw node. node.type = 'VerbatimExpression' node.verbatim = node.text return try program = esprima.parse node.text expression = program.body[0].expression context.replace expression catch e errorNode = ion.clone node, true errorNode.loc?.start.line += e.lineNumber - 1 errorNode.loc?.start.column += e.column - 1 + "`".length message = e.message.substring(e.message.indexOf(':') + 1).trim() throw context.error message, errorNode functionDeclarations = (node, context) -> if node.type is 'VariableDeclaration' and node.kind is 'const' and node.declarations.length is 1 and node.declarations[0].init?.type is 'FunctionExpression' and node.declarations[0].init.id?.name is node.declarations[0].id.name # convert to a FunctionDeclaration for conciseness. func = node.declarations[0].init func.type = 'FunctionDeclaration' context.replace func # make sure there are no empty function expression statements if node.type is 'ExpressionStatement' and node.expression.type is 'FunctionExpression' throw context.error 'Function Expression is a noop', node letAndConstToVar = (node, context) -> if node.type is 'VariableDeclaration' and node.kind isnt 'var' node.kind = 'var' activateStatements = (node, context) -> if node.type is 'ActivateStatement' context.replace type: 'ExpressionStatement' expression: type: 'CallExpression' callee: type: 'MemberExpression' object: type: 'CallExpression' callee: node.argument arguments: [] property: type: 'Identifier' name: 'observe' arguments: [] hoistVariables = (node, context) -> # if node.type is 'BlockStatement' and context.reactive # # sort all children, put variables to the top. # node.body.sort( # (a, b) -> # aValue = if a.type is 'VariableDeclaration' then 1 else 0 # bValue = if b.type is 'VariableDeclaration' then 1 else 0 # return bValue - aValue # ) # hoistES6Variables = (node, context) -> # if node.type is 'BlockStatement' # es6Vars = [] # for child in node.body # if child.type is 'VariableDeclaration' and child.kind isnt 'var' # es6Vars.push child # console.dir es6Vars, {depth: null} variableDeclarationExpressions = (node, context) -> if node.type is 'VariableDeclarationExpression' # extract the variable declaration statement context.addStatement 0, type: 'VariableDeclaration' declarations: node.declarations kind: node.kind # replace this with a reference to the declared variable context.replace node.declarations[0].id addPropertyDeclaration = (node, context) -> if node.type is 'Property' and node.add parentNode = context.parentNode() if not (parentNode.type is 'ObjectExpression') throw context.error "property assignment only valid within ObjectExpression", node temp = context.getVariable prefix: "_" + (node.key.name ? "value") init: node.value tempId = temp.declarations[0].id # replace with variable declaration context.replace temp # then addition of variable to parent context.insertAfter type: 'ExpressionStatement' expression: tempId # then property assignment to variable context.insertAfter type: 'Property' key: node.key value: tempId propertyDefinitions = (node, context) -> if node.type is 'ObjectExpression' # get all properties that have a define definitions = null for property in node.properties if property.define definitions ?= [] definitions.push(property) if definitions? # remove from properties for def in definitions node.properties.remove(def) def.define = false context.replace type: 'CallExpression' callee: getPathExpression 'Object.defi