pankod/refine

View on GitHub
packages/codemod/src/transformations/refine1-to-refine2.ts

Summary

Maintainability
C
1 day
Test Coverage
import { API, JSCodeshift, Collection, FileInfo } from "jscodeshift";
import fs from "fs";
import path from "path";
import { install } from "../helpers";
import { checkPackageLock } from "../helpers";

export const parser = "tsx";

function addRouterProvider(j: JSCodeshift, root: Collection<any>) {
  const routerProviderImports = root.find(j.ImportDeclaration, {
    source: {
      value: "@pankod/refine-react-router",
    },
  });

  if (routerProviderImports.length === 0) {
    // Import route provider
    root
      .find(j.ImportDeclaration, {
        source: {
          value: "@pankod/refine",
        },
      })
      .insertAfter(
        j.importDeclaration(
          [j.importDefaultSpecifier(j.identifier("routerProvider"))],
          j.literal("@pankod/refine-react-router"),
        ),
      );
  } else {
    console.log(
      "WARNING: A router provider from @pankod/refine-react-router is already imported. This tool will not make any migration for router provider.",
    );
    return;
  }

  const refineRoots = root.find(j.JSXElement, {
    openingElement: {
      name: {
        name: "Refine",
      },
    },
  });

  refineRoots.replaceWith((path) => {
    const openingElement = path.node.openingElement;

    let routesExpression: any;

    const routesAttributeIndex = openingElement.attributes?.findIndex(
      (attribute) =>
        attribute.type === "JSXAttribute" &&
        attribute.name.type === "JSXIdentifier" &&
        attribute.name.name === "routes",
    );

    const routerProviderAttributeIndex = openingElement.attributes?.findIndex(
      (attribute) =>
        attribute.type === "JSXAttribute" &&
        attribute.name.type === "JSXIdentifier" &&
        attribute.name.name === "routerProvider",
    );

    if (routerProviderAttributeIndex > -1) {
      console.log(
        "WARNING: There is already a `routerProvider` attribute on Refine component. This tool will not make any migration for router provider.",
      );
      return;
    }

    if (routesAttributeIndex > -1) {
      const routesAttribute = openingElement.attributes?.[routesAttributeIndex];

      if (routesAttribute?.type === "JSXAttribute") {
        const attributeValue = routesAttribute.value;

        if (attributeValue?.type === "JSXExpressionContainer") {
          routesExpression = attributeValue.expression;
        }
      }
    }

    if (routesExpression) {
      openingElement.attributes?.push(
        j.jsxAttribute(
          j.jsxIdentifier("routerProvider"),
          j.jsxExpressionContainer(
            j.objectExpression([
              j.spreadElement(j.identifier("routerProvider")),
              j.property("init", j.identifier("routes"), routesExpression),
            ]),
          ),
        ),
      );

      if (routesAttributeIndex > -1) {
        openingElement.attributes?.splice(routesAttributeIndex, 1);
      }
    } else {
      openingElement.attributes?.push(
        j.jsxAttribute(
          j.jsxIdentifier("routerProvider"),
          j.jsxExpressionContainer(j.identifier("routerProvider")),
        ),
      );
    }

    return path.node;
  });
}

const moveResources = (j: JSCodeshift, root: Collection<any>) => {
  const newResources: { [key: string]: any }[] = [];

  const resourceElements = root.find(j.JSXElement, {
    openingElement: {
      name: {
        name: "Resource",
      },
    },
  });

  const rootElements = root.find(j.JSXElement, {
    openingElement: {
      name: {
        name: "Refine",
      },
    },
  });

  const resourceImportSpecifiers = root.find(j.ImportSpecifier, {
    imported: {
      name: "Resource",
    },
  });

  if (resourceElements.length === 0) {
    return;
  }

  // Get resources data
  resourceElements.forEach((resources) => {
    const newResource: { [key: string]: any } = {};

    resources.node.openingElement.attributes?.forEach((resource) => {
      if (
        resource.type === "JSXAttribute" &&
        resource.name.type === "JSXIdentifier" &&
        resource.value
      ) {
        newResource[resource.name.name] = resource.value;
      }
    });

    newResources.push(newResource);
  });

  // Construct a resources attribute with the resources data
  const newAttributes = j.jsxAttribute(
    j.jsxIdentifier("resources"),
    j.jsxExpressionContainer(
      j.arrayExpression(
        newResources.map((resource) => {
          const newValue = j.objectExpression(
            Object.entries(resource).map(([key, value]) => {
              const valueToPut = value.expression ? value.expression : value;
              return j.property("init", j.identifier(key), valueToPut as any);
            }),
          );

          return newValue;
        }),
      ),
    ),
  );

  // Add resources attribute
  rootElements.replaceWith((path) => {
    const attributes = path.node.openingElement.attributes;

    const resourcesAttributeIndex = attributes.findIndex(
      (attribute) =>
        attribute.type === "JSXAttribute" &&
        attribute.name.name === "resources",
    );

    if (resourcesAttributeIndex !== -1) {
      console.log(
        "WARNING: There is already a 'resources' attribute on Refine component. This tool will not touch it.",
      );

      return path.node;
    }

    attributes?.push(newAttributes);

    return path.node;
  });

  resourceElements.remove();
  resourceImportSpecifiers.remove();

  // Clear the body of Refine component
  rootElements.replaceWith((path) => {
    const openingElement = path.node.openingElement;

    path.node.closingElement = null;
    openingElement.selfClosing = true;

    return path.node;
  });
};

const packagesToUpdate = [
  "@pankod/refine-airtable",
  "@pankod/refine-graphql",
  "@pankod/refine-hasura",
  "@pankod/refine-nestjsx-crud",
  "@pankod/refine-nextjs-router",
  "@pankod/refine-react-router",
  "@pankod/refine-simple-rest",
  "@pankod/refine-strapi",
  "@pankod/refine-strapi-graphql",
  "@pankod/refine-supabase",
];

export async function postTransform(files: any, flags: any) {
  const rootDir = path.join(process.cwd(), files[0]);
  const packageJsonPath = path.join(rootDir, "package.json");
  const useYarn = checkPackageLock(rootDir) === "yarn.lock";
  let packageJsonData;

  try {
    packageJsonData = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
  } catch (err) {
    console.error(
      `Error: failed to load package.json from ${packageJsonPath}, ensure provided directory is root.`,
    );
  }

  const dependenciesToInstall: Array<{
    name: string;
    version: string;
  }> = [
    {
      name: "@pankod/refine",
      version: "2.x.x",
    },
    {
      name: "@pankod/refine-react-router",
      version: "2.x.x",
    },
  ];

  for (const key of Object.keys(packageJsonData.dependencies)) {
    if (packagesToUpdate.includes(key)) {
      dependenciesToInstall.push({
        name: key,
        version: "2.x.x",
      });
    }
  }

  if (!flags.dry) {
    await install(
      rootDir,
      dependenciesToInstall.map((dep) => `${dep.name}@${dep.version}`),
      {
        useYarn,
        isOnline: true,
      },
    );
  }
}

export default function transformer(file: FileInfo, api: API): string {
  const j = api.jscodeshift;
  const source = j(file.source);

  const rootElement = source.find(j.JSXElement, {
    openingElement: {
      name: {
        name: "Refine",
      },
    },
  });

  if (rootElement.length === 0) {
    return;
  }

  addRouterProvider(j, source);
  moveResources(j, source);

  return source.toSource();
}