Skip to content

Commit

Permalink
Allow migrator to target subcomponents and change prop values (#10071)
Browse files Browse the repository at this point in the history
Closes #10084

First pass at allowing prop values to be changed through migrations. 
Also needed to allow support for finding compound components in the
rename prop migration

**Question**
Should this be merged into main? Or left in next until it's tested?
  • Loading branch information
kyledurand authored Aug 17, 2023
1 parent e6a2d35 commit 23c1391
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-wombats-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris-migrator': minor
---

Added support for compound components and adding new prop values in the react-rename-component-prop migration
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';

interface MyComponentProps {
prop?: string;
newProp?: string;
children?: React.ReactNode;
}

const Child = (props: {prop: string}) => <>{props.prop}</>;

function MyComponent(props: MyComponentProps) {
const value = props.newProp ?? props.prop;
return <div data-prop={value}>{props.children}</div>;
}

export function App() {
return (
<MyComponent prop="value">
Hello
<Child prop="value" />
</MyComponent>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';

interface MyComponentProps {
prop?: string;
newProp?: string;
children?: React.ReactNode;
}

const Child = (props: {prop: string}) => <>{props.prop}</>;

function MyComponent(props: MyComponentProps) {
const value = props.newProp ?? props.prop;
return <div data-prop={value}>{props.children}</div>;
}

export function App() {
return (
<MyComponent newProp="new value">
Hello
<Child prop="value" />
</MyComponent>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

interface MyComponentProps {
prop?: string;
newProp?: string;
children?: React.ReactNode;
}

const Child = (props: {prop: string}) => <>{props.prop}</>;

function MyComponent(props: MyComponentProps) {
const value = props.newProp ?? props.prop;
return <div data-prop={value}>{props.children}</div>;
}

function SubComponent({...props}: any) {
return <div {...props} />;
}

export function App() {
return (
<MyComponent>
<MyComponent.SubComponent prop="value" />
Hello
<Child prop="value" />
</MyComponent>
);
}

MyComponent.SubComponent = SubComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

interface MyComponentProps {
prop?: string;
newProp?: string;
children?: React.ReactNode;
}

const Child = (props: {prop: string}) => <>{props.prop}</>;

function MyComponent(props: MyComponentProps) {
const value = props.newProp ?? props.prop;
return <div data-prop={value}>{props.children}</div>;
}

function SubComponent({...props}: any) {
return <div {...props} />;
}

export function App() {
return (
<MyComponent>
<MyComponent.SubComponent newProp="new value" />
Hello
<Child prop="value" />
</MyComponent>
);
}

MyComponent.SubComponent = SubComponent;
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
import {check} from '../../../utilities/check';

const transform = 'react-rename-component-prop';
const fixtures = ['react-rename-component-prop'];

for (const fixture of fixtures) {
check(__dirname, {
fixture,
transform,
const fixtures = [
{
name: 'react-rename-component-prop',
options: {
componentName: 'MyComponent',
from: 'prop',
to: 'newProp',
},
},
{
name: 'react-rename-component-prop-with-new-value',
options: {
componentName: 'MyComponent',
from: 'prop',
to: 'newProp',
newValue: 'new value',
},
},
{
name: 'react-rename-compound-component-prop-with-new-value',
options: {
componentName: 'MyComponent.SubComponent',
from: 'prop',
to: 'newProp',
newValue: 'new value',
},
},
];

for (const fixture of fixtures) {
check(__dirname, {
fixture: fixture.name,
transform,
options: fixture.options,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ export default function transformer(
{jscodeshift: j}: API,
options: Options,
) {
if (!options.componentName || !options.from || !options.to) {
throw new Error('Missing required options: componentName, from, to');
const componentParts = options.componentName?.split('.');
if (
!options.componentName ||
!options.from ||
!options.to ||
componentParts?.length > 2
) {
throw new Error(
'Missing required options: componentName, from, to, or your compound component exceeds 2 levels',
);
}

const source = j(file.source);
const componentName = options.componentName;
const props = {[options.from]: options.to};

renameProps(j, source, componentName, props);
renameProps(j, source, componentName, props, options.newValue);

return source.toSource();
}
50 changes: 41 additions & 9 deletions polaris-migrator/src/utilities/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,24 +123,56 @@ export function replaceJSXElement(
}

export function renameProps(
_j: core.JSCodeshift,
j: core.JSCodeshift,
source: Collection<any>,
componentName: string,
props: {[from: string]: string},
newValue?: string,
) {
const fromProps = Object.keys(props);
const isFromProp = (prop: unknown): prop is keyof typeof props =>
fromProps.includes(prop as string);

source.findJSXElements(componentName)?.forEach((path) => {
path.node.openingElement.attributes?.forEach((node) => {
if (node.type === 'JSXAttribute' && isFromProp(node.name.name)) {
node.name.name = props[node.name.name];
const [component, subcomponent] = componentName.split('.');

// Handle compound components
if (component && subcomponent) {
source.find(j.JSXElement).forEach((element) => {
if (
element.node.openingElement.name.type === 'JSXMemberExpression' &&
element.node.openingElement.name.object.type === 'JSXIdentifier' &&
element.node.openingElement.name.object.name === component &&
element.node.openingElement.name.property.name === subcomponent
) {
element.node.openingElement.attributes?.forEach((node) =>
updateNode(node, props, newValue),
);
}
});
return;
}

// Handle basic components
source.findJSXElements(componentName)?.forEach((element) => {
element.node.openingElement.attributes?.forEach((node) =>
updateNode(node, props, newValue),
);
});

return source;

function updateNode(
node: any,
props: {[from: string]: string},
newValue?: string,
) {
const isFromProp = (prop: unknown): prop is keyof typeof props =>
Object.keys(props).includes(prop as string);

if (node.type !== 'JSXAttribute' && !isFromProp(node.name.name)) {
return node;
}

node.name.name = props[node.name.name];
node.value = j.stringLiteral(newValue ?? node.value.value);
return node;
}
}

export function insertJSXComment(
Expand Down

0 comments on commit 23c1391

Please sign in to comment.