UNPKG

babel-transform-config

Version:

Transform JS config files (Next, Nuxt, Gatsby)

164 lines (141 loc) 5.55 kB
const consola = require('consola') const toAst = require('./toAst') const { dedupeStringLiterals, testNodeValue, buildSubjacentPaths } = require('./utils') const OPERATIONS = ['create', 'merge', 'replace', 'delete'] function mergePaths(parentKeys, nodeName) { return `${parentKeys}${parentKeys.length ? ':' : ''}${nodeName}` } function validateAction(transform) { const operations = transform.action.split(':') operations.forEach((operation) => { if (!OPERATIONS.includes(operation)) { throw new Error(`Operation "${operation}" does not exist.\nDefined operations: ${OPERATIONS}`) } }) if (operations.includes('merge') && operations.includes('replace')) { throw new Error('Operations "merge" and "update" cannot coexist in transform\'s "action" property') } if (operations.includes('create') && operations.includes('delete')) { throw new Error('Operations "create" and "delete" cannot coexist in transform\'s "action" property') } if (operations.includes('merge') && !Array.isArray(transform.value)) { throw new Error('Operations "merge" expects value to be of type "Array" (tested with Array.isArray)') } } function validateTransforms(transforms) { Object.entries(transforms).forEach(([key, transform]) => { if (!transform.action || !transform.action.length) { throw new Error(`Transformation with key "${key}" should possess a non-empty "action" key`) } if (transform.action.indexOf('delete') === -1 && transform.value === undefined) { throw new Error(`Transformation with key "${key}" should possess a non-empty "value" key`) } validateAction(transform) }) } module.exports = function({ types: t }, transforms) { const status = {} validateTransforms(transforms) Object.keys(transforms).forEach((key) => status[key] = false) const expressionVisitor = { ObjectExpression(path, { isRoot, objectKeysPath, createKey, value }) { const fullPathToBuild = mergePaths(objectKeysPath, createKey) const currentParentKey = path.parent.key ? path.parent.key.name : '' const currentPathToBuild = mergePaths(currentParentKey, createKey) if (currentPathToBuild === fullPathToBuild) { if ( path.parent.declaration && path.parent.declaration.properties && path.parent.declaration.properties.find(e => e.key.name === createKey) ) { return } // Make sure you create root key // at exportDefault level to prevent writing value evrywhere if (isRoot && (!path.parentPath.node.key || path.parentPath.node.key.loc)) { return } const newObjectProperty = t.ObjectProperty( t.identifier(createKey), toAst(t, value) ) path.node.properties = [ ...path.node.properties, newObjectProperty ] } } } const objectPropVisitor = { ObjectProperty(path, { parentKeys = '' } =  {}) { const currentPath = mergePaths(parentKeys, path.node.key.name) const transform = transforms[currentPath] if (transform && !status[currentPath]) { status[currentPath] = true const operations = transform.action.split(':') if (operations.includes('delete')) { return path.remove() } const { type } = path.node.value const elemExists = testNodeValue(t, path); (function handleWrite() { if ((!elemExists && operations.includes('create')) || operations.includes('replace')) { path.node.value = toAst(t, transform.value) } if (operations.includes('merge')) { const accessor = type === 'ArrayExpression' ? 'elements' : 'properties' const elems = [ ...path.node.value[accessor], ...toAst(t, transform.value)[accessor] ]; path.node.value[accessor] = dedupeStringLiterals(elems) } })(); } if (path.node.value && (path.node.value.properties || path.node.value.elements)) { return path.traverse(objectPropVisitor, { parentKeys: currentPath }) } } } return { name: 'babel-plugin-transform-config', visitor: { Program(path) { const exportPath = path.get('body') .find((path) => path.isExportDefaultDeclaration() && path.node.declaration && path.node.declaration.type === 'ObjectExpression' ) if (!exportPath) { return consola.error('Could not find default exported object. Maybe your config file returns a function?') } exportPath.traverse(objectPropVisitor) Object.entries(status).forEach(([key, value]) => { if (value === false && transforms[key].action.indexOf('create') !== -1) { const subPaths = key.split(':') const subPathsToCreate = buildSubjacentPaths(subPaths).slice(0, -1); subPathsToCreate.forEach((p, i) => { exportPath.traverse(expressionVisitor, { objectKeysPath: p.slice(0, -1).join(':'), createKey: p.pop(), value: {}, isRoot: i === 0 }) }) exportPath.traverse(expressionVisitor, { objectKeysPath: key.split(':').slice(0, -1).join(':'), createKey: key.split(':').pop(), value: transforms[key].value }) } }, []) }, } } };