import yaml from 'yaml-ast-parser' import fs from 'fs' import _ from 'lodash' import os from 'os' import utils from '@serverlessinc/sf-core/src/utils.js' const { log } = utils const findKeyChain = (astContent) => { let content = astContent const chain = [content.key.value] while (content.parent) { content = content.parent if (content.key) { chain.push(content.key.value) } } return chain.reverse().join('.') } const parseAST = (ymlAstContent, astObject) => { let newAstObject = astObject || {} if (ymlAstContent.mappings && Array.isArray(ymlAstContent.mappings)) { ymlAstContent.mappings.forEach((v) => { if (!v.value) { log.error( `Your serverless.yml has an invalid value with key: "${v.key.value}"`, ) return } if (v.key.kind === 0 && v.value.kind === 0) { newAstObject[findKeyChain(v)] = v.value } else if ( v.key.kind === 0 && (v.value.kind === 2 || v.value.kind === 3) ) { newAstObject[findKeyChain(v)] = v.value newAstObject = parseAST(v.value, newAstObject) } }) } else if (ymlAstContent.items && Array.isArray(ymlAstContent.items)) { ymlAstContent.items.forEach((v, i) => { if (v.kind === 0) { const key = `${findKeyChain(ymlAstContent.parent)}[${i}]` newAstObject[key] = v } }) } return newAstObject } const constructPlainObject = (ymlAstContent, branchObject) => { const newbranchObject = branchObject || {} if (ymlAstContent.mappings && Array.isArray(ymlAstContent.mappings)) { ymlAstContent.mappings.forEach((v) => { if (!v.value) { // no need to log twice, parseAST will log errors return } if (v.key.kind === 0 && v.value.kind === 0) { newbranchObject[v.key.value] = v.value.value } else if (v.key.kind === 0 && v.value.kind === 2) { newbranchObject[v.key.value] = constructPlainObject(v.value, {}) } else if (v.key.kind === 0 && v.value.kind === 3) { const plainArray = [] v.value.items.forEach((c) => { plainArray.push(c.value) }) newbranchObject[v.key.value] = plainArray } }) } return newbranchObject } const addNewArrayItem = (ymlFile, pathInYml, newValue) => fs.readFileAsync(ymlFile, 'utf8').then((yamlContent) => { const rawAstObject = yaml.load(yamlContent) const astObject = parseAST(rawAstObject) const plainObject = constructPlainObject(rawAstObject) const pathInYmlArray = pathInYml.split('.') let currentNode = plainObject for (let i = 0; i < pathInYmlArray.length - 1; i++) { const propertyName = pathInYmlArray[i] const property = currentNode[propertyName] if (!property || _.isObject(property)) { currentNode[propertyName] = property || {} currentNode = currentNode[propertyName] } else { throw new Error(`${property} can only be undefined or an object!`) } } const arrayPropertyName = _.last(pathInYmlArray) let arrayProperty = currentNode[arrayPropertyName] if (!arrayProperty || Array.isArray(arrayProperty)) { arrayProperty = arrayProperty || [] } else { throw new Error(`${arrayProperty} can only be undefined or an array!`) } currentNode[arrayPropertyName] = _.union(arrayProperty, [newValue]) const branchToReplaceName = pathInYmlArray[0] const newObject = {} newObject[branchToReplaceName] = plainObject[branchToReplaceName] const newText = yaml.dump(newObject) if (astObject[branchToReplaceName]) { const beginning = yamlContent.substring( 0, astObject[branchToReplaceName].parent.key.startPosition, ) const end = yamlContent.substring( astObject[branchToReplaceName].endPosition, yamlContent.length, ) return fs.writeFileAsync(ymlFile, `${beginning}${newText}${end}`) } return fs.writeFileAsync(ymlFile, `${yamlContent}${os.EOL}${newText}`) }) const removeExistingArrayItem = async (ymlFile, pathInYml, removeValue) => fs.readFileAsync(ymlFile, 'utf8').then((yamlContent) => { const rawAstObject = yaml.load(yamlContent) const astObject = parseAST(rawAstObject) if (astObject[pathInYml] && astObject[pathInYml].items) { const plainObject = constructPlainObject(rawAstObject) const pathInYmlArray = pathInYml.split('.') let currentNode = plainObject const pathInObjectTree = [] for (let i = 0; i < pathInYmlArray.length - 1; i++) { pathInObjectTree.push(currentNode) currentNode = currentNode[pathInYmlArray[i]] } const arrayPropertyName = _.last(pathInYmlArray) const arrayProperty = currentNode[arrayPropertyName] _.pull(arrayProperty, removeValue) if (!arrayProperty.length) { delete currentNode[arrayPropertyName] pathInObjectTree.push(currentNode) for (let i = pathInObjectTree.length - 1; i > 0; i--) { if (Object.keys(pathInObjectTree[i]).length > 0) { break } delete pathInObjectTree[i - 1][pathInYmlArray[i - 1]] } } const headObjectPath = pathInYmlArray[0] let newText = '' if (plainObject[headObjectPath]) { const newObject = {} newObject[headObjectPath] = plainObject[headObjectPath] newText = yaml.dump(newObject) } const beginning = yamlContent.substring( 0, astObject[headObjectPath].parent.key.startPosition, ) const end = yamlContent.substring( astObject[pathInYml].endPosition, yamlContent.length, ) return fs.writeFileAsync(ymlFile, `${beginning}${newText}${end}`) } return Promise.resolve() }) export default { addNewArrayItem, removeExistingArrayItem, }