@prismicio/babel-transform-config
Advanced tools
Comparing version 0.0.5 to 0.0.6-alpha.ea631c8
73
index.js
@@ -52,33 +52,27 @@ const consola = require('consola') | ||
if (!frameworkTable) { | ||
return consola.error(`[transform-configs] ${framework} Framework not supported\nUse babel plugin directly instead`) | ||
throw new Error(`[transform-configs] ${framework} Framework not supported\nUse babel plugin directly instead`) | ||
} | ||
try { | ||
const transforms = Object.entries(args).reduce((acc, [k, v]) => { | ||
if (frameworkTable[k]) { | ||
const [key, value] = frameworkTable[k](v) | ||
return { | ||
...acc, | ||
[key]: value | ||
} | ||
const transforms = Object.entries(args).reduce((acc, [k, v]) => { | ||
if (frameworkTable[k]) { | ||
const [key, value] = frameworkTable[k](v) | ||
return { | ||
...acc, | ||
[key]: value | ||
} | ||
keysNotFound.push(k) | ||
if (!strict) { | ||
return { | ||
...acc, | ||
[k]: { | ||
action: "create:merge", | ||
value: v | ||
} | ||
} | ||
keysNotFound.push(k) | ||
if (!strict) { | ||
return { | ||
...acc, | ||
[k]: { | ||
action: "create:merge", | ||
value: v | ||
} | ||
} | ||
return acc | ||
}, {}) | ||
return { | ||
transforms, | ||
keysNotFound | ||
} | ||
} catch(e) { | ||
return { | ||
reason: e | ||
} | ||
return acc | ||
}, {}) | ||
return { | ||
transforms, | ||
keysNotFound | ||
} | ||
@@ -104,15 +98,18 @@ } | ||
function transformConfig(code, framework, args, strict = true) { | ||
const { | ||
transforms, | ||
keysNotFound, | ||
reason, | ||
} = createTransformArgs(framework, args, strict) | ||
try { | ||
const { | ||
transforms, | ||
keysNotFound, | ||
} = createTransformArgs(framework, args, strict) | ||
if (!transforms) { | ||
return consola.error(reason) | ||
if (!transforms) { | ||
return consola.error(reason) | ||
} | ||
if (!strict && keysNotFound.length) { | ||
handleKeysNotFound(keysNotFound); | ||
} | ||
return transform(code, transforms) | ||
} catch(e) { | ||
consola.error(e.message); | ||
} | ||
if (!strict && keysNotFound.length) { | ||
handleKeysNotFound(keysNotFound); | ||
} | ||
return transform(code, transforms) | ||
} | ||
@@ -123,2 +120,2 @@ | ||
module.exports = transformConfig | ||
module.exports = transformConfig |
{ | ||
"name": "@prismicio/babel-transform-config", | ||
"version": "0.0.5", | ||
"version": "0.0.6-alpha.ea631c8", | ||
"description": "Transform JS config files (Next, Nuxt, Gatsby)", | ||
@@ -18,2 +18,5 @@ "main": "index.js", | ||
"author": "Hugo Villain", | ||
"contributors": [ | ||
"Arnaud lewis (@arnaudlewis)" | ||
], | ||
"license": "ISC", | ||
@@ -20,0 +23,0 @@ "dependencies": { |
@@ -1,164 +0,114 @@ | ||
const consola = require('consola') | ||
const toAst = require('./toAst') | ||
const consola = require('consola'); | ||
const Actions = require('./actions'); | ||
const Trees = require('./tree').Trees; | ||
const ArrayHelpers = require('./utils').ArrayHelpers; | ||
const toAst = require('./toAst'); | ||
const Operations = require('./operations'); | ||
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) | ||
return Object.entries(transforms).map(([key, transform]) => { | ||
return Actions.validate(key, transform.action, transform.value) | ||
}) | ||
} | ||
module.exports = function({ types: t }, transforms) { | ||
const status = {} | ||
const nodeVisitor = { | ||
ObjectExpression(path, state) { | ||
const { nodeData: parentData, globalTypes } = state; | ||
const namedParent = path.parent.key && path.parent.key.name; | ||
validateTransforms(transforms) | ||
Object.keys(transforms).forEach((key) => status[key] = false) | ||
const currentData = namedParent && parentData && parentData.nextNodes.find(n => n.key === namedParent); | ||
if(currentData) { | ||
// detect and create missing nodes | ||
const childrenKeys = path.node.properties.map(node => node.key.name); | ||
const missingKeys = ArrayHelpers.diff(currentData.nextNodes.map(n => n.key), childrenKeys); | ||
if(missingKeys.length) { | ||
missingKeys.forEach(key => { | ||
const newObjectProperty = globalTypes.ObjectProperty( | ||
globalTypes.identifier(key), | ||
toAst(globalTypes, {}) | ||
); | ||
const expressionVisitor = { | ||
ObjectExpression(path, { isRoot, objectKeysPath, createKey, value }) { | ||
path.node.properties = [ | ||
...path.node.properties, | ||
newObjectProperty | ||
]; | ||
}); | ||
} | ||
// update current node if needed | ||
if(currentData.ops.includes(Operations.delete && !currentData.value) && !currentData.nextNodes.length) { | ||
path.remove(); | ||
} else if(currentData.value && currentData.ops.includes(Operations.merge)) { | ||
if(path.node.value) { | ||
const { type } = path.node.value; | ||
switch(type) { | ||
case 'ArrayExpression': | ||
const elements = path.node.elements.concat(toAst(globalTypes, currentData.value).elements); | ||
const updated = Object.assign({}, path.node, { elements }); | ||
path.replaceWithMultiple([ | ||
updated | ||
]); | ||
break; | ||
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 | ||
default: | ||
const properties = path.node.properties.concat(toAst(globalTypes, currentData.value).properties); | ||
const updated = Object.assign({}, path.node, { properties }); | ||
path.replaceWith(updated); | ||
} | ||
} else { | ||
path.replaceWith(toAst(globalTypes, currentData.value)); | ||
} | ||
} else if(currentData.value && (currentData.ops.includes(Operations.replace) || currentData.ops.includes(Operations.create))) { | ||
path.replaceWith(toAst(globalTypes, currentData.value)); | ||
} | ||
// 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 | ||
] | ||
} | ||
// keep exploring with a subset of the model based on the current visited node | ||
path.skip(); | ||
path.traverse(nodeVisitor, { nodeData: currentData, globalTypes }) | ||
} | ||
} | ||
const objectPropVisitor = { | ||
ObjectProperty(path, { parentKeys = '' } = {}) { | ||
const currentPath = mergePaths(parentKeys, path.node.key.name) | ||
const transform = transforms[currentPath] | ||
}, | ||
ArrayExpression(path, state) { | ||
const { nodeData: parentData, globalTypes } = state; | ||
const namedParent = path.parent.key && path.parent.key.name; | ||
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) | ||
} | ||
else 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) | ||
} | ||
})(); | ||
const currentData = namedParent && parentData && parentData.nextNodes.find(n => n.key === namedParent); | ||
// update current node if needed | ||
if(currentData) { | ||
if(currentData.ops.includes(Operations.delete && !currentData.value) && !currentData.nextNodes.length) { | ||
path.remove(); | ||
} else if(currentData.value && currentData.ops.includes(Operations.merge)) { | ||
const elements = path.node.elements.concat(toAst(globalTypes, currentData.value).elements); | ||
const updated = Object.assign({}, path.node, { elements }); | ||
path.replaceWithMultiple([ | ||
updated | ||
]); | ||
} else if(currentData.value && (currentData.ops.includes(Operations.replace) || currentData.ops.includes(Operations.create))) { | ||
path.replaceWithMultiple([ | ||
toAst(globalTypes, currentData.value) | ||
]); | ||
} | ||
if (path.node.value && (path.node.value.properties || path.node.value.elements)) { | ||
return path.traverse(objectPropVisitor, { | ||
parentKeys: currentPath | ||
}) | ||
} | ||
// keep exploring with a subset of the model based on the current visited node | ||
path.skip(); | ||
path.traverse(nodeVisitor, { nodeData: currentData, globalTypes }); | ||
} | ||
} | ||
} | ||
module.exports = function({ types: globalTypes }, transforms) { | ||
const validActions = validateTransforms(transforms); | ||
const data = validActions | ||
.map(Actions.convertToTree) | ||
.reduce((accTree, actionTree) => { | ||
return accTree.combine(actionTree) | ||
}, Trees.empty()); | ||
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 | ||
}) | ||
} | ||
}, []) | ||
}, | ||
ExportDefaultDeclaration(path) { | ||
path.traverse(nodeVisitor, { nodeData: data.root, globalTypes }); | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
} |
@@ -1,40 +0,48 @@ | ||
function buildSubjacentPaths(arr, store = [], len = 0) { | ||
if (len >= arr.length) { | ||
return store | ||
} | ||
store = [...store, arr.slice(0, len + 1)] | ||
return buildSubjacentPaths(arr, store, len + 1) | ||
} | ||
const ArrayHelpers = { | ||
splitAtLast(array) { | ||
if(!array) return null; | ||
function dedupeStringLiterals(elements) { | ||
const set = new Set() | ||
return elements.filter(e => { | ||
if (e.type !== 'StringLiteral') { | ||
return true | ||
switch(array.length) { | ||
case 0: return [[], null]; | ||
case 1: return [[], array[0]]; | ||
default: return [array.slice(0, -1), array[array.length - 1]] | ||
} | ||
const duplicate = set.has(e.value) | ||
set.add(e.value) | ||
return !duplicate | ||
}) | ||
} | ||
}, | ||
/** There probably is a better way */ | ||
function testNodeValue(t, path) { | ||
const { type } = path.node.value | ||
if (type === 'ArrayExpression') { | ||
return !!path.node.value.elements.length | ||
splitAtHead(array) { | ||
if(!array) return null; | ||
switch(array.length) { | ||
case 0: return [null, []]; | ||
case 1: return [array[0], []]; | ||
default: return [array[0], array.slice(1)] | ||
} | ||
}, | ||
combine(array1, array2, mergeFn) { | ||
const [main, other] = array1.length > array2.length ? [array2, array1] : [array1, array2]; | ||
return main | ||
.map((value, index) => mergeFn(value, other[index])) | ||
.concat(other.slice(main.length, other.length).map(value => mergeFn(undefined, value))) | ||
}, | ||
flatten(arr) { | ||
return arr.reduce((acc, subArr) => acc.concat(subArr), []); | ||
}, | ||
distinct(arr1, arr2, predicate) { | ||
this.flatten( | ||
this.combine(arr1, arr2, (item1, item2) => predicate(item1, item2) ? [item1] : [item1, item2]) | ||
); | ||
}, | ||
diff(array1, array2) { | ||
return array1.filter(function(elm) { | ||
return array2.indexOf(elm) === -1; | ||
}) | ||
} | ||
if (type === 'ObjectExpression') { | ||
return !!path.node.value.properties.length | ||
} | ||
if (type === 'NullLiteral') { | ||
return false | ||
} | ||
return true | ||
} | ||
module.exports = { | ||
buildSubjacentPaths, | ||
dedupeStringLiterals, | ||
testNodeValue | ||
} | ||
ArrayHelpers | ||
} |
20561
15
533