UNPKG

hakk

Version:

Interactive programming for Node.js. Speed up your JavaScript development!

904 lines (848 loc) 31.4 kB
const generate = require('@babel/generator').default; const parser = require('@babel/parser'); const template = require('@babel/template').default; const traverse = require('@babel/traverse').default; const types = require('@babel/types'); const { sha256 } = require('./utils.js'); // TODO: const hoistVariables = require('@babel/helper-hoist-variables').default; // Parse JS code into a babel ast. const parse = (code) => parser.parse(code, { sourceType: 'module' }); // ## AST transformation visitors const copyLocation = (fromNode, toNode) => { toNode.start = fromNode.start; toNode.end = fromNode.end; toNode.loc = fromNode.loc; }; const getEnclosingFunction = path => path.findParent((path) => path.isFunction()); const getEnclosingVariableDeclarator = path => path.findParent((path) => path.isVariableDeclarator()); const findNestedIdentifierValues = (node) => { const identifierValuesFound = []; if (types.isObjectPattern(node)) { for (const property of node.properties) { if (types.isIdentifier(property.value)) { identifierValuesFound.push(property.value.name); } else { const moreValuesFound = findNestedIdentifierValues(property.value); identifierValuesFound.push(...moreValuesFound); } } } else if (types.isArrayPattern(node)) { for (const element of node.elements) { if (types.isIdentifier(element)) { identifierValuesFound.push(element.name); } else { const moreValuesFound = findNestedIdentifierValues(element); identifierValuesFound.push(...moreValuesFound); } } } else if (types.isIdentifier(node)) { identifierValuesFound.push(node.name); } return identifierValuesFound; }; const handleAwaitExpression = (path) => { if (getEnclosingFunction(path)) { return; } const topPath = path.find((path) => path.parentPath.isProgram()); topPath.node._topLevelAwait = true; const declarator = getEnclosingVariableDeclarator(path); if (declarator) { if (!types.isProgram(declarator.parentPath.parentPath)) { return; } const identifierNames = findNestedIdentifierValues(declarator.node.id); const syncDeclarator = template.ast(`var ${identifierNames.join(', ')};`); copyLocation(declarator.node, syncDeclarator); // TODO: Make more precise by pointing to individual identifiers: for (const declaration of syncDeclarator.declarations) { copyLocation(declarator.node, declaration); copyLocation(declarator.node, declaration.id); } const asyncAssignment = { type: 'ExpressionStatement', expression: { type: 'AssignmentExpression', operator: '=', left: declarator.node.id, right: declarator.node.init } }; copyLocation(declarator.node, asyncAssignment); const outputs = [ syncDeclarator, asyncAssignment ]; declarator.parentPath.replaceWithMultiple(outputs); } }; const handleForOfStatement = (path) => { if (getEnclosingFunction(path)) { return; } if (path.node.await) { const topPath = path.find((path) => path.parentPath.isProgram()); topPath.node._topLevelForOfAwait = true; } }; const awaitVisitor = { AwaitExpression (path) { handleAwaitExpression(path); }, ForOfStatement (path) { handleForOfStatement(path); } }; const handleVariableDeclarationEnter = (path) => { if (!types.isProgram(path.parentPath)) { return; } if (path.node.kind !== 'var' || path.node.declarations.length > 1) { const outputNodes = path.node.declarations.map( d => types.variableDeclaration('var', [d])); for (let i = 0; i < path.node.declarations.length; ++i) { copyLocation(path.node.declarations[i], outputNodes[i]); } path.replaceWithMultiple(outputNodes); } }; const handleVariableDeclarationExit = (path) => { if (!types.isProgram(path.parentPath)) { return; } const identifierValues = []; for (const declarator of path.node.declarations) { identifierValues.push(...findNestedIdentifierValues(declarator.id)); } path.node._definedVars = identifierValues; path.node._removeCode = identifierValues .map(identifier => `${identifier} = undefined;`) .join('\n'); }; const handleCallExpressionWithRequireOrImport = (path) => { if (path.node.callee && types.isImport(path.node.callee)) { path.node.callee.type = 'Identifier'; path.node.callee.name = '__import'; } if (getEnclosingFunction(path)) { return; } const topPath = path.find((path) => path.parentPath.isProgram()); if (path.node.callee && path.node.arguments[0]) { const firstArgument = path.node.arguments[0].value; if (path.node.callee.name === '__import') { topPath.node._topLevelImport = firstArgument; } if (path.node.callee.name === 'require') { topPath.node._topLevelRequire = firstArgument; } } }; const varVisitor = { VariableDeclaration: { enter (path) { handleVariableDeclarationEnter(path); }, exit (path) { handleVariableDeclarationExit(path); } }, CallExpression: { exit (path) { handleCallExpressionWithRequireOrImport(path); } } }; const handleImportDotMeta = (path) => { if (path.node.meta.name === 'import') { const originalPathNode = path.node; path.replaceWith(template.ast('__import.meta')); copyLocation(originalPathNode, path.node); copyLocation(originalPathNode.meta, path.node.object); copyLocation(originalPathNode.property, path.node.property); } }; const handleImportDeclaration = (path) => { const source = path.node.source.value; const specifiers = []; let namespaceId; for (const specifier of path.node.specifiers) { if (specifier.type === 'ImportDefaultSpecifier') { specifiers.push(`default: ${specifier.local.name}`); } else if (specifier.type === 'ImportSpecifier') { if (specifier.imported.type === 'Identifier' && specifier.imported.name !== specifier.local.name) { specifiers.push( `${specifier.imported.name}: ${specifier.local.name}`); } else if (specifier.imported.type === 'StringLiteral' && specifier.imported.value !== specifier.local.name) { specifiers.push( `'${specifier.imported.value}': ${specifier.local.name}`); } else { specifiers.push(specifier.local.name); } } else if (specifier.type === 'ImportNamespaceSpecifier') { namespaceId = specifier.local.name; } } const sourceString = `await __import('${source}')`; let line = ''; if (namespaceId !== undefined) { line += `const ${namespaceId} = ${sourceString};`; } if (specifiers.length > 0) { line += `const {${specifiers.join(', ')}} = ${namespaceId ?? sourceString};`; } if (namespaceId === undefined && specifiers.length === 0) { line = sourceString; } const newAst = template.ast(line); if (namespaceId && specifiers.length > 0) { copyLocation(path.node, newAst[0]); path.replaceWithMultiple(newAst); } else { copyLocation(path.node, newAst); if (newAst.declarations) { for (let i = 0; i < newAst.declarations.length; ++i) { copyLocation(path.node.specifiers[i], newAst.declarations[i]); } } path.replaceWith(newAst); } }; const importVisitor = { MetaProperty (path) { handleImportDotMeta(path); }, ImportDeclaration (path) { handleImportDeclaration(path); } }; const getEnclosingClass = path => path.findParent((path) => path.isClassDeclaration()); const getEnclosingSuperClassName = path => getEnclosingClass(path).node.superClass.name; const getEnclosingMethod = path => path.findParent((path) => path.isMethod()); const getEnclosingProperty = path => path.findParent((path) => path.isProperty()); const isTopLevelDeclaredObject = (path) => types.isVariableDeclarator(path.parentPath) && types.isVariableDeclaration(path.parentPath.parentPath) && types.isProgram(path.parentPath.parentPath.parentPath); const handleCallExpressionEnter = (path) => { if (path.node.callee.type !== 'MemberExpression' || path.node.callee.object.type !== 'Super') { return; } const methodPath = getEnclosingMethod(path); if (!methodPath || methodPath.kind === 'constructor') { return; } // if (TODO: !isTopLevelDeclaredObject(getEnclosingClass(path))) { // return; // } const methodName = path.node.callee.property.name; const isStatic = methodPath.node.static; const superClassName = getEnclosingSuperClassName(path); const ast = template.ast(`${superClassName}${isStatic ? '' : '.prototype'}.${methodName}.call(${isStatic ? '' : 'this'})`); copyLocation(path.node, ast); const expressionAST = ast.expression; expressionAST.arguments = expressionAST.arguments.concat(path.node.arguments); path.replaceWith(expressionAST); }; const handleMemberExpression = (path) => { const object = path.node.object; if (object.type !== 'Super') { // || // TODO: !isTopLevelDeclaredObject(getEnclosingClass(path))) { return; } const enclosure = getEnclosingProperty(path) ?? getEnclosingMethod(path); const superClassName = getEnclosingSuperClassName(path); if (enclosure.node.static) { object.type = 'Identifier'; object.name = superClassName; } else { // For instance methods, super.property access should access the parent's prototype property // since instance fields are stored as prototype properties with WeakMap-based getters/setters const propertyName = path.node.property.name; const ast = template.ast(`${superClassName}.prototype.${propertyName}`); copyLocation(path.node, ast); path.replaceWith(ast); } }; // Convert private methods and fields to public methods // and fields with a `_PRIVATE_` prefix. const handlePrivateProperty = (path, propertyType) => { path.node.type = propertyType; path.node.key.type = 'Identifier'; path.node.key.name = '_PRIVATE_' + path.node.key.id.name; path.node.key.id = undefined; }; const nodesForClass = ({ className, classBodyNodes }) => { const outputNodes = []; const retainedNodes = []; for (const classBodyNode of classBodyNodes) { let templateAST; // Convert methods and fields declarations to separable // assignment statements. if (classBodyNode.type === 'ClassMethod') { if (classBodyNode.kind === 'constructor') { retainedNodes.push(classBodyNode); } else if (classBodyNode.kind === 'method' || classBodyNode.kind === 'get' || classBodyNode.kind === 'set') { const keyExpression = classBodyNode.computed ? generate(classBodyNode.key).code : classBodyNode.key.name; const target = classBodyNode.static ? className : `${className}.prototype`; let fun; if (classBodyNode.kind === 'method') { const propertyAccess = classBodyNode.computed ? `[${keyExpression}]` : `.${keyExpression}`; templateAST = template.ast( `${target}${propertyAccess} = ${classBodyNode.async ? 'async ' : ''}function${classBodyNode.generator ? '*' : ''} () {}` ); fun = templateAST.expression.right; } else { const propertyName = classBodyNode.computed ? keyExpression : `"${keyExpression}"`; templateAST = template.ast( `Object.defineProperty(${target}, ${propertyName}, { ${classBodyNode.kind}: function () { }, configurable: true });` ); fun = templateAST.expression.arguments[2].properties[0].value; } fun.body = classBodyNode.body; fun.params = classBodyNode.params; copyLocation(classBodyNode, templateAST); templateAST._removeCode = `if (${className}) { delete ${target}${classBodyNode.computed ? '[' + keyExpression + ']' : '.' + keyExpression} }`; } else { throw new Error(`Unexpected ClassMethod kind ${classBodyNode.kind}`); } } else if (classBodyNode.type === 'ClassProperty') { const keyExpression = classBodyNode.computed ? generate(classBodyNode.key).code : classBodyNode.key.name; if (classBodyNode.static) { // Static fields go on the class constructor const target = className; const propertyAccess = classBodyNode.computed ? `[${keyExpression}]` : `.${keyExpression}`; templateAST = template.ast( `${target}${propertyAccess} = undefined;` ); copyLocation(classBodyNode, templateAST); if (classBodyNode.value !== null) { templateAST.expression.right = classBodyNode.value; } templateAST._removeCode = `if (${className}) { delete ${target}${classBodyNode.computed ? '[' + keyExpression + ']' : '.' + keyExpression} }`; } else { // Instance fields: values are stored in a WeakMap const propertyName = classBodyNode.computed ? keyExpression : `"${keyExpression}"`; templateAST = template.ast( `(function (initValue) { const valueMap = new WeakMap(); Object.defineProperty(${className}.prototype, ${propertyName}, { get() { return valueMap.has(this) ? valueMap.get(this) : initValue; }, set(newValue) { valueMap.set(this, newValue); }, configurable: true }); })(undefined);` ); templateAST.expression.arguments[0] = classBodyNode.value || types.identifier('undefined'); copyLocation(classBodyNode, templateAST); templateAST._removeCode = `delete ${className}.prototype[${propertyName}];`; } } else if (classBodyNode.type === 'StaticBlock') { templateAST = template.ast( `(function () {}).call(${className})` ); copyLocation(classBodyNode, templateAST); templateAST.expression.callee.object.body = { type: 'BlockStatement', body: classBodyNode.body }; } else { throw new Error(`Unexpected ClassBody node type ${classBodyNode.type}`); } if (templateAST !== undefined) { outputNodes.push(templateAST); } } outputNodes.forEach(function (outputNode) { outputNode._parentLabel = className; }); return { retainedNodes, outputNodes }; }; const handleClassExpression = (path) => { // Only do top-level class variable declarations. if (!isTopLevelDeclaredObject(path)) { return; } const classNode = path.node; let className, classBodyNodes; if (types.isVariableDeclarator(path.parentPath)) { className = path.parentPath.node.id.name; } if (types.isClassBody(classNode.body)) { classBodyNodes = classNode.body.body; } const { retainedNodes, outputNodes } = nodesForClass( { classNode, className, classBodyNodes }); classNode.body.body = retainedNodes; path.parentPath.parentPath.node._segmentLabel = className; path.parentPath.parentPath.insertAfter(outputNodes); }; const handleClassDeclaration = (path) => { // Only modify top-level class declarations. if (!types.isProgram(path.parentPath)) { return; } // Convert a class declaration into a class expression bound to a var. const classNode = path.node; const expression = template.ast('var AClass = class AClass { }'); const declaration = expression.declarations[0]; declaration.id.name = classNode.id.name; declaration.init.id.name = classNode.id.name; declaration.init.body = classNode.body; declaration.init.superClass = classNode.superClass; copyLocation(classNode, expression); copyLocation(classNode, declaration.init); copyLocation(classNode.id, declaration.id); path.replaceWith(expression); }; const handlePrivateName = (path) => { path.replaceWith(path.node.id); path.node.name = '_PRIVATE_' + path.node.name; }; // Make class declarations mutable by transforming to class // expressions assigned to a var, with member declarations // hoisted out of the class body. const classVisitor = { PrivateName: { enter (path) { handlePrivateName(path); } }, ClassPrivateMethod (path) { handlePrivateProperty(path, 'ClassMethod'); }, ClassPrivateProperty (path) { handlePrivateProperty(path, 'ClassProperty'); }, ClassExpression: { exit (path) { handleClassExpression(path); } }, ClassDeclaration: { enter (path) { handleClassDeclaration(path); } } }; const handleFunctionDeclaration = (path) => { if (!types.isProgram(path.parentPath)) { return; } const functionNode = path.node; const expression = template.ast('var aFunction = function aFunction () {}'); const declaration = expression.declarations[0]; declaration.id.name = functionNode.id.name; declaration.init.id.name = functionNode.id.name; declaration.init.body = functionNode.body; declaration.init.async = functionNode.async; declaration.init.generator = functionNode.generator; declaration.init.params = functionNode.params; copyLocation(functionNode, expression); copyLocation(functionNode, declaration); copyLocation(functionNode.id, declaration.id); copyLocation(functionNode, declaration.init); path.replaceWith(expression); }; const handleFunctionExpression = (path) => { const grandparentPath = path.parentPath.parentPath; if (!types.isVariableDeclaration(grandparentPath) || !types.isProgram(grandparentPath.parentPath)) { return; } const name = path.parentPath.node.id.name; if (grandparentPath.node._dontWrap) { return; } const implName = name + '_hakk_'; path.parentPath.node.id.name = implName; const wrapperAST = template.ast( `var ${name} = (...args) => ${implName}(...args);`); wrapperAST._dontWrap = true; copyLocation(path.parentPath.node, wrapperAST.declarations[0]); grandparentPath.insertAfter(wrapperAST); }; const functionVisitor = { FunctionDeclaration: { enter (path) { handleFunctionDeclaration(path); } }, FunctionExpression: { enter (path) { handleFunctionExpression(path); } }, ArrowFunctionExpression: { enter (path) { handleFunctionExpression(path); } } }; const superVisitor = { CallExpression: { enter (path) { handleCallExpressionEnter(path); } }, MemberExpression (path) { handleMemberExpression(path); } }; const handleObjectExpression = (path) => { if (!isTopLevelDeclaredObject(path)) { return; } const originalProperties = path.node.properties; // Check if the object contains any spread elements // If it does, don't transform it at all - spread elements are runtime operations const hasSpreadElements = originalProperties.some(property => types.isSpreadElement(property)); if (hasSpreadElements) { // Don't transform objects with spread elements - preserve the original object literal return; } const name = path.parentPath.node.id.name; const outputASTs = []; let identifierNames = []; if (name !== undefined) { path.node.properties = []; } else { identifierNames = findNestedIdentifierValues(path.parentPath.node.id); const declarator = template.ast(`var ${identifierNames.join(', ')};`); path.parentPath.node.init = undefined; path.parentPath.parentPath.replaceWith(declarator); } for (const property of originalProperties) { const key = property.key; let ast; let keyExpression; if (types.isObjectProperty(property)) { // Use generate() for all key types - handles Identifier, StringLiteral, MemberExpression, BinaryExpression, etc. keyExpression = generate(key).code; if (name === undefined) { if (identifierNames.includes(keyExpression)) { ast = template.ast(`${keyExpression} = undefined;`); } } else { // Use bracket notation for all computed properties, dot notation for simple identifiers if (property.computed || !types.isIdentifier(key)) { ast = template.ast(`${name}[${keyExpression}] = undefined;`); } else { ast = template.ast(`${name}.${keyExpression} = undefined;`); } } if (ast) { copyLocation(property, ast); ast.expression.right = property.value; copyLocation(property, ast.expression); copyLocation(property.key, ast.expression.left); } } else if (types.isObjectMethod(property)) { // Use generate() for all key types keyExpression = generate(key).code; if (property.kind === 'get' || property.kind === 'set') { // Handle getters and setters if (name === undefined) { if (identifierNames.includes(keyExpression)) { ast = template.ast(`Object.defineProperty(${keyExpression}, "${keyExpression}", { ${property.kind}: function () { }, configurable: true });`); } } else { // For Object.defineProperty, we need the property name as a string literal const propertyName = types.isIdentifier(key) ? `"${key.name}"` : keyExpression; ast = template.ast(`Object.defineProperty(${name}, ${propertyName}, { ${property.kind}: function () { }, configurable: true });`); } if (ast) { copyLocation(property, ast); const accessor = ast.expression.arguments[2].properties[0].value; accessor.body = property.body; accessor.params = property.params; } } else { // Handle regular methods if (name === undefined) { if (identifierNames.includes(keyExpression)) { ast = template.ast(`${keyExpression} = function () { };`); } } else { // Use bracket notation for computed properties, dot notation for simple identifiers if (property.computed || !types.isIdentifier(key)) { ast = template.ast(`${name}[${keyExpression}] = function () { };`); } else { ast = template.ast(`${name}.${keyExpression} = function () { };`); } } if (ast) { copyLocation(property, ast); const expressionRight = ast.expression.right; expressionRight.params = property.params; expressionRight.async = property.async; expressionRight.generator = property.generator; expressionRight.body = property.body; copyLocation(property.body, ast.expression); copyLocation(property.key, ast.expression.left); } } } else { throw new Error(`Unexpected object member '${property.type}'.`); } if (ast) { if (types.isIdentifier(key)) { ast._removeCode = `delete ${name || key.name}['${key.name}']`; } else { ast._removeCode = `delete ${name}[${keyExpression}]`; } outputASTs.push(ast); } } path.parentPath.parentPath.insertAfter(outputASTs); }; const objectVisitor = { ObjectExpression (path) { handleObjectExpression(path); } }; const astCodeToAddToModuleExports = (identifier, localName) => types.isStringLiteral(identifier) ? template.ast(`module.exports['${identifier.value}'] = ${localName}`) : template.ast(`module.exports.${identifier.name} = ${localName}`); const wildcardExport = (namespaceIdentifier) => { const namespaceAccessorString = namespaceIdentifier ? (types.isStringLiteral(namespaceIdentifier) ? `['${namespaceIdentifier.value}']` : `.${namespaceIdentifier.name}`) : ''; return template.ast( `const propertyNames = Object.getOwnPropertyNames(importedObject); for (const propertyName of propertyNames) { if (propertyName !== 'default') { module.exports${namespaceAccessorString}[propertyName] = importedObject[propertyName]; } }`); }; const wrapImportedObject = (moduleName, asts) => { const resultAST = template.ast( `await (async function () { const importedObject = await __import('${moduleName}'); })();`); resultAST.expression.argument.callee.body.body.push(...asts); return resultAST; }; const handleExportNameDeclaration = (path) => { const outputASTs = []; const specifiers = path.node.specifiers; const declaration = path.node.declaration; if (specifiers && specifiers.length > 0 && (declaration === null || declaration === undefined)) { const specifierASTs = []; const source = path.node.source; for (const specifier of specifiers) { if (types.isExportSpecifier(specifier)) { const localName = `${source ? 'importedObject.' : ''}${specifier.local.name}`; const resultsAST = astCodeToAddToModuleExports(specifier.exported, localName); copyLocation(specifier, resultsAST); specifierASTs.push(resultsAST); } else if (types.isExportNamespaceSpecifier(specifier)) { specifierASTs.push(...wildcardExport(specifier.exported)); copyLocation(specifier, specifiers); } } if (source) { const moduleName = path.node.source.value; const resultAST = wrapImportedObject(moduleName, specifierASTs); outputASTs.push(resultAST); } else { outputASTs.push(...specifierASTs); } } else if (specifiers.length === 0 && declaration !== null) { outputASTs.push(declaration); if (types.isVariableDeclaration(declaration)) { for (const declarator of declaration.declarations) { if (types.isObjectPattern(declarator.id)) { const objectName = declarator.init.name; for (const property of declarator.id.properties) { const resultsAST = astCodeToAddToModuleExports(property.value, `${objectName}.${property.key.name}`); copyLocation(property, resultsAST); outputASTs.push(resultsAST); } } else if (types.isArrayPattern(declarator.id)) { let i = 0; const arrayName = declarator.init.name; for (const element of declarator.id.elements) { const resultsAST = astCodeToAddToModuleExports(element, `${arrayName}[${i}]`); copyLocation(element, resultsAST); outputASTs.push(resultsAST); ++i; } } else if (types.isIdentifier(declarator.id)) { const resultsAST = astCodeToAddToModuleExports(declarator.id, declarator.id.name); copyLocation(declarator, resultsAST); outputASTs.push(resultsAST); } } } if (types.isFunctionDeclaration(declaration) || types.isClassDeclaration(declaration)) { const identifier = declaration.id; const resultsAST = astCodeToAddToModuleExports(identifier, identifier.name); copyLocation(declaration, resultsAST); outputASTs.push(resultsAST); } } if (outputASTs.length > 0) { copyLocation(path.node, outputASTs[0]); } path.replaceWithMultiple(outputASTs); }; const handleExportDefaultDeclaration = (path) => { const outputAST = template.ast('module.exports.default = undefined'); outputAST.expression.right = path.node.declaration; copyLocation(path.node, outputAST); path.replaceWith(outputAST); }; const handleExportAllDeclaration = (path) => { const moduleName = path.node.source.value; const lines = wildcardExport(null); path.replaceWith(wrapImportedObject(moduleName, lines)); }; const exportVisitor = { ExportNamedDeclaration: { exit (path) { handleExportNameDeclaration(path); } }, ExportDefaultDeclaration: { exit (path) { handleExportDefaultDeclaration(path); } }, ExportAllDeclaration: { exit (path) { handleExportAllDeclaration(path); } } }; const transform = (ast, visitors) => { for (const visitor of visitors) { traverse(ast, visitor); } return ast; }; const functionDeclarationsFirst = (nodes) => { const head = []; const tail = []; for (const node of nodes) { if (types.isFunctionDeclaration(node)) { head.push(node); } else { tail.push(node); } } return [...head, ...tail]; }; const prepareAST = (code) => { if (code.trim().length === 0) { return ''; } const ast = parse(code); ast.program.body = functionDeclarationsFirst(ast.program.body); return transform(ast, [importVisitor, exportVisitor, superVisitor, objectVisitor, classVisitor, awaitVisitor, functionVisitor, varVisitor]); }; const prepareCode = (code) => { if (code.length === 0) { return ''; } else { return generate(prepareAST(code)).code; } }; const prepareAstNodes = (code) => { if (code.length === 0) { return []; } else { const program = prepareAST(code).program; return program ? program.body : []; } }; const findCodeToRemove = (previousNodes, addedOrChangedVarsSeen) => { // Removal code for previousNodes that haven't been found in new version. const toRemove = []; for (const node of previousNodes.values()) { if (node._removeCode) { const deletedVars = node._definedVars ? node._definedVars.filter(v => !addedOrChangedVarsSeen.includes(v)) : undefined; // Remove in reverse order: toRemove.unshift({ code: node._removeCode, isAsync: false, deletedVars }); } } return toRemove; }; const findVarsToDeclare = (addedOrChangedVarsSeen) => { // Any defined variables should be declared at the top because sometimes // vars are referenced forward. const toDeclare = []; if (addedOrChangedVarsSeen.length > 0) { const declareFirstFragment = { code: `var ${addedOrChangedVarsSeen.join(', ')};`, isAsync: false }; toDeclare.unshift(declareFirstFragment); } return toDeclare; }; const getCodeAndOriginalOffset = (node, filePath) => { const { code: codeRaw, rawMappings } = generate(node, { comments: true, retainLines: true, sourceMaps: true, sourceFileName: filePath }, ''); const code = codeRaw.trim(); let originalOffset; try { originalOffset = rawMappings[0].original.line; } catch (e) { console.log('Failed to compute offset: ', code, node); originalOffset = 0; } return { code, originalOffset }; }; const changedNodesToCodeFragments = (previousNodes, nodes, filePath) => { const currentNodes = new Map(); const updatedParentLabels = new Set(); const toWrite = []; const addedOrChangedVarsSeen = []; const offsetsMap = {}; for (const node of nodes) { const { code, originalOffset } = getCodeAndOriginalOffset(node, filePath); const codeNormalized = code.replace(/\s+/g, ' '); const codeHash = sha256(filePath + '\n' + code).substring(0, 16); const tracker = filePath + '|' + codeHash; offsetsMap[codeHash] = originalOffset; currentNodes.set(codeNormalized, node); if (previousNodes.has(codeNormalized) && !(node._parentLabel && updatedParentLabels.has(node._parentLabel))) { previousNodes.delete(codeNormalized); } else { if (node._segmentLabel) { updatedParentLabels.add(node._segmentLabel); } toWrite.push({ code, isAsync: node._topLevelAwait || node._topLevelForOfAwait, addedOrChangedVars: node._definedVars, tracker }); addedOrChangedVarsSeen.push(...(node._definedVars ?? [])); } } const toRemove = findCodeToRemove(previousNodes, addedOrChangedVarsSeen); const toDeclare = findVarsToDeclare(addedOrChangedVarsSeen); const fragments = [...toDeclare, ...toRemove, ...toWrite]; return { fragments, latestNodes: currentNodes, offsetsMap }; }; module.exports = { generate, parse, prepareAstNodes, prepareCode, prepareAST, changedNodesToCodeFragments };