react18-tools/react-mouse-trail

View on GitHub
scripts/rc.js

Summary

Maintainability
A
0 mins
Test Coverage
const fs = require("fs");
const path = require("path");

const __dir = path.resolve(__dirname, "..");
const TEMPLATE_DIR = "scripts/templates/";

/**
 * @typedef {Object} InquirerDataType
 * @property {boolean} isClient - Indicates whether a client component should be created.
 * @property {string} name - Component name along with the relative path from either the client or server directory.
 * @property {string} pkgPath - Package path.
 */

/**
 * Updates index files if needed based on the provided parameters.
 * @param {import('plop').ActionType} nestedRouteActions - Nested route actions.
 * @param {string[]} rootSegments - Root segments.
 * @param {string[]} currentDirSegments - Current directory segments.
 * @param {boolean} isClient - Indicates whether it's a client component.
 */
function updateIndexFilesIfNeeded(nestedRouteActions, rootSegments, currentDirSegments, isClient) {
  const indexFilePath = path.resolve(__dir, ...rootSegments, ...currentDirSegments, "index.ts");
  const root = rootSegments.join("/");
  if (!fs.existsSync(indexFilePath)) {
    const content =
      `${isClient ? '"use client";\n' : ""}// ${currentDirSegments.join("/")}` +
      " component exports\n";
    nestedRouteActions.push({
      type: "add",
      path: `${root + currentDirSegments.join("/")}/index.ts`,
      template: content,
    });
    const length = currentDirSegments.length;
    nestedRouteActions.push({
      type: "append",
      pattern: /(?<insertion> component exports)/,
      path: `${
        root + (length === 1 ? "" : `${currentDirSegments.slice(0, length - 1).join("/")}/`)
      }index.ts`,
      template: `export * from "./${currentDirSegments[length - 1]}"`,
    });
  }
}

/**
 * Converts a string to kebab-case.
 * @param {string} str - The input string.
 * @returns {string} The string in kebab-case.
 */
function toKebabCase(str) {
  return str
    .trim()
    .replace(/([a-z])([A-Z])/g, "$1-$2")
    .toLowerCase()
    .replace(/ +/g, "-");
}

/**
 * createRootIndexAndDeclarations if not present.
 * @param {InquirerDataType} data - Input data.
 */
function createRootIndexAndDeclarations(data) {
  const nestedRouteActions = [];
  const { isClient } = data;
  const srcDir = path.resolve(__dir, `${data.pkgPath}/src`);
  const [banner, target] = isClient ? ['"use client";\n\n', "client"] : ["", "server"];
  const root = `${data.pkgPath}/src/${target}/`;

  /** Create index.ts in src directory if not present.  */
  if (!fs.existsSync(path.resolve(srcDir, "index.ts")))
    nestedRouteActions.push({
      type: "add",
      path: `${data.pkgPath}/src/index.ts`,
      template: `${banner}export * from "./${target}";\n`,
    });

  /** Create declaration if not present.  */
  if (!fs.existsSync(path.resolve(srcDir, "declaration.d.ts")))
    nestedRouteActions.push({
      type: "add",
      path: `${data.pkgPath}/src/declaration.d.ts`,
      template: 'declare module "*.module.css";\ndeclare module "*.module.scss";\n',
    });

  /** Create index.ts in src/client or src/server directory if not present.  */
  if (!fs.existsSync(path.resolve(__dir, root, "index.ts")))
    nestedRouteActions.push({
      type: "add",
      path: `${root}index.ts`,
      template: `${banner}/**\n * Server components and client components need to be exported from separate files as\n * directive on top of the file from which component is imported takes effect.\n * i.e., server component re-exported from file with "use client" will behave as client component\n */\n\n// ${target} component exports\n`,
    });

  return { nestedRouteActions, root, isClient };
}

/**
 * Gets nested route actions based on the provided data.
 * @param {InquirerDataType} data - Input data.
 * @returns {Object} Nested route actions and parent directory.
 */
function getNestedRouteActions(data) {
  const name = data.name.replace(/\/+/g, "/").replace(/\/$/, "").trim();
  const { nestedRouteActions, root, isClient } = createRootIndexAndDeclarations(data);

  if (!name.includes("/")) return { nestedRouteActions, parentDir: root };

  const lastSlashInd = name.lastIndexOf("/") || name.lastIndexOf("\\");
  data.name = name.slice(lastSlashInd + 1);

  const directories = toKebabCase(name.slice(0, lastSlashInd)).split(/\/|\\/);
  const rootSegments = [...root.split(/\/|\\/)];

  for (let i = 1; i <= directories.length; i++)
    updateIndexFilesIfNeeded(nestedRouteActions, rootSegments, directories.slice(0, i), isClient);

  return { nestedRouteActions, parentDir: `${root + directories.join("/")}/` };
}

/**
 * Gets the index action based on the provided data and parent directory.
 * @param {InquirerDataType} data - Input data.
 * @param {string} parentDir - Parent directory.
 * @returns {Object} Index action.
 */
function getIndexAction(data, parentDir) {
  const indFilePath = path.resolve(__dir, parentDir, toKebabCase(data.name), "index.ts");
  if (fs.existsSync(indFilePath))
    return {
      type: "append",
      pattern: /(?<insertion> component exports)/,
      path: `${parentDir}{{kebabCase name}}/index.ts`,
      template: 'export * from "./{{kebabCase name}}";',
    };
  return {
    type: "add",
    path: `${parentDir}{{kebabCase name}}/index.ts`,
    template: `${data.isClient ? '"use client";\n\n' : ""}// component exports\nexport * from "./{{kebabCase name}}";\n`,
  };
}

/**
 * Gets actions based on the provided data.
 * @param {InquirerDataType} data - Input data.
 * @returns {Array} Actions.
 */
function getActions(data) {
  const { nestedRouteActions, parentDir } = getNestedRouteActions(data);

  return nestedRouteActions.concat([
    getIndexAction(data, parentDir),
    {
      type: "add",
      path: `${parentDir}{{kebabCase name}}/{{kebabCase name}}.tsx`,
      templateFile: `${TEMPLATE_DIR}component.hbs`,
    },
    {
      type: "add",
      path: `${parentDir}{{kebabCase name}}/{{kebabCase name}}.test.tsx`,
      templateFile: `${TEMPLATE_DIR}component.test.hbs`,
    },
    {
      type: "add",
      path: `${parentDir}{{kebabCase name}}/{{kebabCase name}}.module.scss`,
      templateFile: `${TEMPLATE_DIR}component.module.hbs`,
    },
    {
      type: "append",
      pattern: /(?<insertion> component exports)/,
      path: `${parentDir}index.ts`,
      template: 'export * from "./{{kebabCase name}}";',
    },
  ]);
}

/** Export rc generator */
module.exports = {
  description: "Adds a new React component to the selected package.",
  prompts: [
    {
      type: "list",
      name: "pkgPath",
      choices: ["lib", "packages/shared"],
      default: "lib",
      message: "Select the package",
    },
    {
      type: "input",
      name: "name",
      message: "What is the name of the component?",
    },
    {
      type: "confirm",
      name: "isClient",
      message: 'Is this a client component? (Should we add "use client" directive?)',
    },
    {
      type: "input",
      name: "description",
      message: "Describe your component. (This will be added as js-doc comment.)",
    },
  ],
  actions: data => (data ? getActions(data) : []),
};