admin管理员组

文章数量:1208155

I am trying to write a babel plugin to wrap every component with a Profiler. But it breaks react native.

This is my babel.config.js

module.exports = function (api) {
  api.cache(false)
  return {
    presets: [
      'babel-preset-expo', 'module:metro-react-native-babel-preset'
    ],
    plugins: [
      [ "./babel/babel-plugin-wrap-profiler/plugin.js"],
      [
        'babel-plugin-root-import',
        {
          paths: [
            {
              rootPathPrefix: ':ui/',
              rootPathSuffix: './libs/ui/src/',
            },
            {
              rootPathPrefix: ':data/',
              rootPathSuffix: './libs/data/src/',
            },
            {
              rootPathPrefix: ':utils/',
              rootPathSuffix: './libs/utils/src/',
            },
          ],
        },
      ],
      [
        require.resolve('babel-plugin-module-resolver'),
        {
          alias: {
            ':common': './libs/common/src',
            ':ui': './libs/ui/src',
            ':data': './libs/data/src',
            ':utils': './libs/utils/src',
          },
        },
      ],
      [
        '@emotion',
        {
          // sourceMap is on by default but source maps are dead code eliminated in production
          sourceMap: true,
          autoLabel: 'dev-only',
          labelFormat: '[local]',
          cssPropOptimization: true,
        },
      ],
      ['@babel/plugin-transform-object-assign'],
      ['@babel/plugin-transform-react-jsx-source'],
      ['macros'],
      ['react-native-reanimated/plugin'],
    ],
  }
}

This is the plugin file

import * as fs from "fs"
import * as path from "path"


function checkIfIdentifierDefined(path, identifier) {
  // Parse the code into an AST
  let isDefined = false;

  // Traverse the AST to check for the identifier
  path.traverse({
    VariableDeclarator(path) {
      if (path.node.id.name === identifier) {
        isDefined = true;
      }
    },
    FunctionDeclaration(path) {
      if (path.node.id && path.node.id.name === identifier) {
        isDefined = true;
      }
    },
    // Add other node types as needed (e.g., FunctionExpression, ArrowFunctionExpression, etc.)
  });

  return isDefined;
}

const handleProfilerImport = (t, path) => {
  let profilerFound = false
  let reactImportNode = null
  let profilerImportName = 'Profiler'
  if (checkIfIdentifierDefined(path, "Profiler")) {
    // TODO: import with different name
    return;
  }

  for (const n of path.node.body) {
    if (n.type !== 'ImportDeclaration') continue;
    if (n.source.value !== 'react') continue;

    reactImportNode = n;

    for (const specifier of n.specifiers) {
      if (specifier.imported && specifier.local) {
        if (specifier.imported.name === 'Profiler' || specifier.local.name === 'Profiler') {
          profilerImportName = specifier.local.name;
          profilerFound = true;
          break;
        }

        if (!specifier.imported && specifier.local) {
          if (specifier.local.name === 'Profiler') {
            profilerFound = true;
            break;
          }
        }
      }
    }
  }

  const importSpecifier = t.importSpecifier(
    t.identifier('Profiler'),
    t.identifier('Profiler')
  )


  if (!profilerFound) {
    // react import found
    if (reactImportNode !== null) {
      reactImportNode.specifiers.push(importSpecifier)
    } else {
      // create es6 import
      path.node.body.unshift(
        t.importDeclaration(
          [importSpecifier],
          t.stringLiteral('react')
        )
      )
    }
  }

  return profilerImportName
}

function enclosingFunctionName(path){
  const functionPath = path.findParent((p) =>
    p.isFunctionDeclaration() ||
      p.isFunctionExpression() ||
      p.isArrowFunctionExpression()
  );

  if (!functionPath) return

  const node = functionPath.node;
  if (node.type === 'FunctionDeclaration' && node.id) {
    return node.id.name;
  }

  if (
    (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') &&
    functionPath.parentPath.isVariableDeclarator()
  ) {
    return functionPath.parentPath.node.id.name;
  }

  if (
    node.type === 'FunctionExpression' &&
    functionPath.parentPath.isObjectProperty()
  ) {
    return functionPath.parentPath.node.key.name;
  }

  return "anonymous"
}

const _getComponentName = (scope, path) => {
  if (scope.path.type === "ClassMethod") {
    return scope.path.parentPath.parent.id.name
  }
  if (scope.path.type === "FunctionDeclaration") {
    if (path.parent.type === "ReturnStatement") {
      return enclosingFunctionName(path)
    }
    return scope.path.container.declaration.id.name
  }
  if (scope.path.type === "FunctionExpression") {
    return scope.path.parentPath?.parentPath?.parentPath?.parent?.arguments?.find(arg => arg.type === "Identifier")?.name
  }
  return  (scope.path?.container?.id?.name) ?? path.parent?.id?.name
}

const getComponentName = (scope, path) => {
  return _getComponentName(scope, path) ?? enclosingFunctionName(path)
}

export default function ({ types: t, template }) {

  const wrapWithProfiler = (jsx, componentName) => {
    const Profiler = 'Profiler'
    return template.ast`
React.createElement(
${Profiler},
{
id: '${componentName}',
onRender: onRenderCallBack$
},
${jsx.node}
)
`
  }

  return {
    name: "wrap-profiler",
    visitor: {
      Program: {
        enter(path, state) {
          if (this?.file?.opts?.filename?.includes("node_modules")) {
            return;
          }
          let hasJSX = true

          path.traverse({
            JSXElement: {
              enter() {
                hasJSX = true
              }
            }
          })

          if (!hasJSX) {
            return
          }

          const profilerImport = handleProfilerImport(t, path)

          path.unshiftContainer('body', t.importDeclaration(
            [
              t.importSpecifier(
                t.identifier('onRenderCallBack$'),
                t.identifier('onRenderCallBack$')
              )
            ],
            t.stringLiteral('babel-plugin-wrap-profiler/profiler-utils')
          ))
        }
      },
      JSXElement: {
        enter(path, state) {
          if (this?.file?.opts?.filename?.includes("node_modules")) {
            return;
          }
          const { parent, scope } = path
          const topLevelReactComponent = parent.type === "ReturnStatement" || parent.type === "ArrowFunctionExpression"
          if (topLevelReactComponent) {
            // function component name
            let componentName = getComponentName(scope, path)
            // wrap top level react component with profiler
             const newNodeAst = wrapWithProfiler(path, componentName)
             path.replaceWith(newNodeAst)
            path.skip();
          }
        }
      }
    } 
  } 
}

I run below command to start the project.

rm -rf $TMPDIR/metro-cache && yarn start --clear

I get this error: Compiling JS Failed: import declaration must be at the top level of module.

The error is because of the wrapWithProfiler function but I assumed other plugins that run after me would transform that jsx into actual javascript. But I guess that's not happening, or maybe the wrapWithProfiler function is wrong.

本文标签: babeljsbabel plugin that outputs jsx breaks react nativeStack Overflow