Skip to content

Commit

Permalink
feat(Masthead): Update structure
Browse files Browse the repository at this point in the history
  • Loading branch information
wise-king-sullyman committed Aug 1, 2024
1 parent 578475a commit 4b080a5
Show file tree
Hide file tree
Showing 6 changed files with 406 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Rule } from "eslint";
import {
JSXAttribute,
JSXIdentifier,
JSXMemberExpression,
JSXNamespacedName,
JSXElement,
Expression,
} from "estree-jsx";
import { getAttributeValue } from "./JSXAttributes";

// new interfaces, just local for now to avoid conflicts with the masthead rename RP
interface JSXElementWithParent extends JSXElement {
parent?: JSXElement;
}

// Similar story here, will use the getName helper from my other PR once it goes in
function getName(
nodeName: JSXIdentifier | JSXMemberExpression | JSXNamespacedName
) {
switch (nodeName.type) {
case "JSXIdentifier":
return nodeName.name;
case "JSXMemberExpression":
return getName(nodeName.object);
case "JSXNamespacedName":
return nodeName.namespace.name;
}
}

// same story here, will remove this from this file once the other PR is in and I can merge it into this branch
function getAttributeName(attr: JSXAttribute) {
switch (attr.name.type) {
case "JSXIdentifier":
return attr.name.name;
case "JSXNamespacedName":
return attr.name.name.name;
}
}

/** Gets a string representation of an element including */
export function stringifyJSXElement(
context: Rule.RuleContext,
node: JSXElementWithParent
) {
const { openingElement, children, closingElement } = node;

let str = "<";

str += getName(openingElement.name);

if (openingElement.attributes.length) {
const nonSpreadAttributes = openingElement.attributes.filter(
(attr) => attr.type === "JSXAttribute"
);
nonSpreadAttributes.forEach((attr) => {
const attrName = getAttributeName(attr as JSXAttribute);

const attrValue = getAttributeValue(
context,
(attr as JSXAttribute).value
);

const attrValueWrapper =
typeof attrValue === "string" ? `"${attrValue}"` : `{${attrValue}}`;

str += ` ${attrName}=${attrValueWrapper}`;
});
}

if (openingElement.selfClosing) {
str += "/";
}

str += ">";

children.forEach((child) => {
switch (child.type) {
case "JSXElement":
str += stringifyJSXElement(context, child as JSXElementWithParent);
break;
case "JSXText":
str += child.raw;
break;
case "JSXExpressionContainer":
case "JSXSpreadChild":
case "JSXFragment":
}
});

if (closingElement) {
str += `</${getName(closingElement.name)}>`;
}

return str;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
### masthead-structure-changes [(#10809)](https://github.com/patternfly/patternfly-react/pull/10809)

The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.

#### Examples

In:

```jsx
%inputExample%
```

Out:

```jsx
%outputExample%
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const ruleTester = require("../../ruletester");
import * as rule from "./masthead-structure-changes";

ruleTester.run("masthead-structure-changes", rule, {
valid: [
{
code: `<Masthead />`,
},
{
code: `import { Masthead } from '@patternfly/react-core'; <Masthead someOtherProp />`,
},
],
invalid: [
// stage one of a pre-renamed file
{
code: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand>Bar</MastheadBrand></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; <Masthead><MastheadMain><MastheadToggle>Foo</MastheadToggle><MastheadBrand>Bar</MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
// stage two of a pre-renamed file
{
code: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; <Masthead><MastheadMain><MastheadToggle>Foo</MastheadToggle><MastheadBrand>Bar</MastheadBrand></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadBrand, MastheadMain, MastheadToggle } from '@patternfly/react-core'; <Masthead><MastheadMain><MastheadToggle>Foo</MastheadToggle><MastheadBrand data-codemods><MastheadBrand>Bar</MastheadBrand></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
// stage one of a post-renamed file
{
code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle } from '@patternfly/react-core'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadLogo>Bar</MastheadLogo></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
// stage two of a post-renamed file
{
code: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core'; <Masthead><MastheadToggle>Foo</MastheadToggle><MastheadMain><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
output: `import { Masthead, MastheadLogo, MastheadMain, MastheadToggle, MastheadBrand } from '@patternfly/react-core'; <Masthead><MastheadMain><MastheadToggle>Foo</MastheadToggle><MastheadBrand data-codemods><MastheadLogo>Bar</MastheadLogo></MastheadBrand></MastheadMain></Masthead>`,
errors: [
{
message: `The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.`,
type: "JSXOpeningElement",
},
],
},
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Rule } from "eslint";
import {
ImportDefaultSpecifier,
ImportSpecifier,
JSXAttribute,
JSXElement,
JSXOpeningElement,
} from "estree-jsx";
import { getAllImportsFromPackage, getChildElementByName } from "../../helpers";
import { stringifyJSXElement } from "../../helpers/stringifyJSXElement";
// https://github.com/patternfly/patternfly-react/pull/10809

// new interfaces, just local for now to avoid conflicts with the masthead rename RP
interface JSXElementWithParent extends JSXElement {
parent?: JSXElement;
}

interface JSXOpeningElementWithParent extends JSXOpeningElement {
parent?: JSXElementWithParent;
}

// same story here, will remove this from this file once the other PR is in and I can merge it into this branch
function getAttributeName(attr: JSXAttribute) {
switch (attr.name.type) {
case "JSXIdentifier":
return attr.name.name;
case "JSXNamespacedName":
return attr.name.name.name;
}
}

// same story here, will remove this from this file once the other PR is in and I can merge it into this branch
function hasCodeModDataTag(openingElement: JSXOpeningElement) {
const nonSpreadAttributes = openingElement.attributes.filter(
(attr) => attr.type === "JSXAttribute"
);
const attributeNames = nonSpreadAttributes.map((attr) =>
getAttributeName(attr as JSXAttribute)
);
return attributeNames.includes("data-codemods");
}

function moveNodeIntoMastheadMain(
context: Rule.RuleContext,
fixer: Rule.RuleFixer,
node: JSXOpeningElementWithParent
) {
if (!node.parent || !node.parent.parent) {
return [];
}

const mastheadMain = getChildElementByName(
node.parent.parent,
"MastheadMain"
);

if (!mastheadMain) {
return [];
}

const fixes = [fixer.remove(node.parent)];

const nodeString = stringifyJSXElement(context, node.parent);

fixes.push(fixer.insertTextAfter(mastheadMain.openingElement, nodeString));

return fixes;
}

function wrapNodeInMastheadBrand(
fixer: Rule.RuleFixer,
node: JSXOpeningElementWithParent,
componentImports: (ImportSpecifier | ImportDefaultSpecifier)[]
) {
if (!node.parent) {
return [];
}

const fixes = [];

const closingNode = node.parent?.closingElement
? node.parent.closingElement
: node;

fixes.push(fixer.insertTextBefore(node, "<MastheadBrand data-codemods>"));
fixes.push(fixer.insertTextAfter(closingNode, "</MastheadBrand>"));

const importCount = componentImports.length - 1;
const lastImport = componentImports[importCount];

const namedImports = componentImports.filter(
(imp) => imp.type === "ImportSpecifier"
);
const importNames = namedImports.map(
(imp) => (imp as ImportSpecifier).imported.name
);

if (!importNames.includes("MastheadBrand")) {
fixes.push(fixer.insertTextAfter(lastImport, ", MastheadBrand"));
}

return fixes;
}

module.exports = {
meta: { fixable: "code" },
create: function (context: Rule.RuleContext) {
const componentImports = getAllImportsFromPackage(
context,
"@patternfly/react-core",
["MastheadBrand", "MastheadToggle", "MastheadLogo"]
);

const message =
"The structure of Masthead has been updated, MastheadToggle and MastheadBrand should now be wrapped in MastheadMain.";

return !componentImports.length
? {}
: {
JSXOpeningElement(node: JSXOpeningElementWithParent) {
if (
node.name.type !== "JSXIdentifier" ||
!componentImports
.map((imp) => imp.local.name)
.includes(node.name.name)
) {
return;
}
const parentOpeningElement = node.parent?.parent?.openingElement;

if (!parentOpeningElement) {
return;
}

const nodeName = node.name.name;
const parentName =
parentOpeningElement.name.type === "JSXIdentifier" &&
parentOpeningElement.name.name;

if (
nodeName === "MastheadToggle" &&
parentName !== "MastheadMain"
) {
context.report({
node,
message,
fix: (fixer) => moveNodeIntoMastheadMain(context, fixer, node),
});
return;
}

const isPreRenameMastheadBrand =
nodeName === "MastheadBrand" &&
parentName === "MastheadMain" &&
!hasCodeModDataTag(node);

const isPostRenameMastheadBrand =
nodeName === "MastheadLogo" && parentName !== "MastheadBrand";

if (isPreRenameMastheadBrand || isPostRenameMastheadBrand) {
context.report({
node,
message,
fix: (fixer) =>
wrapNodeInMastheadBrand(fixer, node, componentImports),
});
}
},
};
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
Masthead,
MastheadBrand,
MastheadMain,
MastheadToggle,
MastheadLogo
} from "@patternfly/react-core";

export const MastheadStructureChangesInputPreNameChange = () => (
<Masthead>
<MastheadToggle>Foo</MastheadToggle>
<MastheadMain>
<MastheadBrand>Bar</MastheadBrand>
</MastheadMain>
</Masthead>
);

export const MastheadStructureChangesInputPostNameChange = () => (
<Masthead>
<MastheadToggle>Foo</MastheadToggle>
<MastheadMain>
<MastheadLogo>Bar</MastheadLogo>
</MastheadMain>
</Masthead>
);
Loading

0 comments on commit 4b080a5

Please sign in to comment.