diff --git a/.changeset/kind-rooms-cry.md b/.changeset/kind-rooms-cry.md new file mode 100644 index 00000000000..ba1b1e4a77d --- /dev/null +++ b/.changeset/kind-rooms-cry.md @@ -0,0 +1,5 @@ +--- +"@hashicorp/design-system-codemods": patch +--- + +`dropdown-list-item-interactive` - Reworked codemod to support usage in complex conditionals, and values with concatenated handlebars values. diff --git a/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/concatenated-value.input.hbs b/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/concatenated-value.input.hbs new file mode 100644 index 00000000000..48b48ca484b --- /dev/null +++ b/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/concatenated-value.input.hbs @@ -0,0 +1,7 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + + \ No newline at end of file diff --git a/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/concatenated-value.output.hbs b/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/concatenated-value.output.hbs new file mode 100644 index 00000000000..cc860770f2e --- /dev/null +++ b/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/concatenated-value.output.hbs @@ -0,0 +1,7 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + Edit {{this.name}} + \ No newline at end of file diff --git a/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/inside-complex-conditional.input.hbs b/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/inside-complex-conditional.input.hbs new file mode 100644 index 00000000000..3991174a631 --- /dev/null +++ b/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/inside-complex-conditional.input.hbs @@ -0,0 +1,27 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + {{#if this.showError}} + + {{else if this.showWarning}} + + {{else}} + + {{/if}} + \ No newline at end of file diff --git a/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/inside-complex-conditional.output.hbs b/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/inside-complex-conditional.output.hbs new file mode 100644 index 00000000000..ecdb65ead86 --- /dev/null +++ b/packages/codemods/transforms/v4/dropdown-list-item-interactive/__testfixtures__/inside-complex-conditional.output.hbs @@ -0,0 +1,13 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + {{#if this.showError}} + Error State + {{else if this.showWarning}} + Warning State + {{else}} + Default State + {{/if}} + \ No newline at end of file diff --git a/packages/codemods/transforms/v4/dropdown-list-item-interactive/index.js b/packages/codemods/transforms/v4/dropdown-list-item-interactive/index.js index 2869a752829..4df031af12e 100644 --- a/packages/codemods/transforms/v4/dropdown-list-item-interactive/index.js +++ b/packages/codemods/transforms/v4/dropdown-list-item-interactive/index.js @@ -5,121 +5,78 @@ const CODEMOD_ANALYSIS = process.env.CODEMOD_ANALYSIS; -function processChildren(children, asPrefix, b) { - let hasUpdatedChildren = false; - let processedChildren = []; - - children.forEach((child) => { - let updatedChild; - let isProcessed = false; - - // Check if the child is an ElementNode with the specified tag - if (child.type === 'ElementNode' && child.tag === `${asPrefix}.Interactive`) { - const textAttr = child.attributes.find((a) => a.name === '@text'); - - if (textAttr) { - const childOutputAttributes = child.attributes.filter((a) => a.name !== '@text'); - - const isHandlebarsAttr = textAttr.value.type === 'MustacheStatement'; - - let children = []; - - // Handle different types of MustacheStatement values - if (isHandlebarsAttr) { - if (textAttr.value.path.type === 'NumberLiteral') { - children = [b.mustache(b.number(textAttr.value.path.value))]; - } else if (textAttr.value.path.type === 'StringLiteral') { - children = [b.mustache(b.string(textAttr.value.path.value))]; - } else { - children = [ - b.mustache( - textAttr.value.path.original, - [...textAttr.value.params], - textAttr.value.hash - ), - ]; - } - } else { - children = [b.text(textAttr.value.chars)]; - } - - // Create a new element with the updated children and attributes - updatedChild = b.element( - { name: child.tag, selfClosing: false }, - { - children, - attrs: childOutputAttributes, - modifiers: child.modifiers, - blockParams: child.blockParams, - } - ); - - isProcessed = true; - } else { - updatedChild = child; - } - } else if (child.type === 'BlockStatement') { - // Recursively process children of BlockStatement nodes - const { hasUpdatedChildren: nestedHasUpdated, processedChildren: nestedProcessed } = - processChildren(child.program.body, asPrefix, b); - - if (nestedHasUpdated) { - updatedChild = b.block( - child.path, - child.params, - child.hash, - b.program(nestedProcessed, child.program.blockParams), - child.inverse - ); - isProcessed = true; - } else { - updatedChild = child; - } - } - - processedChildren.push(updatedChild || child); - hasUpdatedChildren = hasUpdatedChildren || isProcessed; - }); - - return { hasUpdatedChildren, processedChildren }; -} - module.exports = function ({ source /*, path*/ }, { parse, visit }) { const ast = parse(source); + // A stack is used to correctly handle nested components + const asPrefixStack = []; + return visit(ast, (env) => { - let { builders: b } = env.syntax; + const { builders: b } = env.syntax; return { - ElementNode(node) { - // Check if the node is an Hds::Dropdown element - if (node.type === 'ElementNode' && node.tag === 'Hds::Dropdown') { - if (node.blockParams && node.blockParams.length > 0) { - const asPrefix = node.blockParams[0]; - - // Process the children of the Hds::Dropdown element - const { hasUpdatedChildren, processedChildren } = processChildren( - node.children, - asPrefix, - b - ); + ElementNode: { + // "enter" is called before visiting the node's children + enter(node) { + // If we encounter a Dropdown, push its `as` parameter onto the stack + if (node.tag === 'Hds::Dropdown') { + if (node.blockParams && node.blockParams.length > 0) { + asPrefixStack.push(node.blockParams[0]); + } else { + // Push a falsy value to keep the stack balanced if there's no block param + asPrefixStack.push(null); + } + } - // Return the updated element if any children were updated - if (hasUpdatedChildren && !CODEMOD_ANALYSIS) { - return [ - b.element( - { name: node.tag, selfClosing: false }, - { - attrs: node.attributes, - children: processedChildren, - modifiers: node.modifiers, - blockParams: node.blockParams, - } - ), - ]; + // Get the current prefix from the top of the stack + const asPrefix = asPrefixStack[asPrefixStack.length - 1]; + + // If there's a prefix and this node is the one we want to transform... + if (!CODEMOD_ANALYSIS && asPrefix && node.tag === `${asPrefix}.Interactive`) { + const textAttr = node.attributes.find((a) => a.name === '@text'); + + if (textAttr) { + const childOutputAttributes = node.attributes.filter((a) => a.name !== '@text'); + const attrValue = textAttr.value; + let newChildren = []; + + if (attrValue.type === 'ConcatStatement') { + newChildren = attrValue.parts; + } else if (attrValue.type === 'MustacheStatement') { + if (attrValue.path.type === 'NumberLiteral') { + newChildren = [b.mustache(b.number(attrValue.path.value))]; + } else if (attrValue.path.type === 'StringLiteral') { + newChildren = [b.mustache(b.string(attrValue.path.value))]; + } else { + newChildren = [ + b.mustache(attrValue.path.original, [...attrValue.params], attrValue.hash), + ]; + } + } else { + newChildren = [b.text(attrValue.chars)]; + } + + // Return a new element to replace the current one + // The visitor will automatically use this returned value + return b.element( + { name: node.tag, selfClosing: false }, + { + children: newChildren, + attrs: childOutputAttributes, + modifiers: node.modifiers, + blockParams: node.blockParams, + } + ); } } - } + }, + // "exit" is called after visiting the node's children + exit(node) { + // As we leave a Dropdown, pop its prefix off the stack + if (node.tag === 'Hds::Dropdown') { + asPrefixStack.pop(); + } + }, }, }; });