@babel/core#PluginObj TypeScript Examples

The following examples show how to use @babel/core#PluginObj. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: babel.ts    From xwind with MIT License 6 votes vote down vote up
function babel(
  babel: typeof Babel,
  config: { config?: string },
  workingPath: string
): PluginObj<PluginPass> {
  const { types: t } = babel;
  const twConfigPath = getTwConfigPath(config.config);
  return {
    name: "xwind",
    visitor: {
      ImportDefaultSpecifier(path, state) {
        //get referencePaths for default import from "xwind" or "xwind/macro"
        if (!path.parentPath.isImportDeclaration()) return;
        if (path.parent.type !== "ImportDeclaration") return;
        if (!MODULES.includes(path.parent.source.value)) return;

        const referencePaths =
          path.scope.bindings[path.node.local.name].referencePaths;

        //remove default import and remove import if no other specifiers exist
        path.remove();
        if (path.parent.specifiers.length === 0) {
          path.parentPath.remove();
        }

        if (!referencePaths.length) return;

        const transformer = getCachedTransformer(twConfigPath);
        transformer(referencePaths, state, t);
      },
    },
  };
}
Example #2
Source File: plugin.ts    From reskript with MIT License 6 votes vote down vote up
plugin = (): PluginObj => {
    return {
        visitor: {
            Program(path) {
                const callee = prepareReactImport(path, 'useEffect');
                path.pushContainer('body', callee);
            },
        },
    };
}
Example #3
Source File: plugin.ts    From telefunc with MIT License 6 votes vote down vote up
export default function BabelPluginTelefunc(babel: { types: typeof BabelTypes }): PluginObj {
  return {
    visitor: {
      Program(path, context) {
        const filename: string = context.filename!

        if (!filename.includes('.telefunc.')) return
        if (isFileAlreadyTransformed(path, babel.types)) return

        const exportList = getExportsFromBabelAST(path, babel.types)

        const root: string = context.file.opts.root!
        const transformed = transformTelefuncFileSync(toPosixPath(filename), toPosixPath(root), exportList).code

        const parsed = parse(transformed, {
          sourceType: 'module'
        })

        path.replaceWith(parsed.program)
      }
    }
  }
}
Example #4
Source File: index.ts    From react-dev-inspector with MIT License 6 votes vote down vote up
export function InspectorBabelPlugin(babel, options?: InspectorPluginOptions): PluginObj {
  return {
    name: 'react-dev-inspector-babel-plugin',

    visitor: createVisitor({
      cwd: options?.cwd,
      excludes: [
        'node_modules/',
        ...options?.excludes ?? [],
      ],
    }),
  }
}
Example #5
Source File: translate.ts    From nota with MIT License 5 votes vote down vote up
optimizePlugin = (): PluginObj => ({
  visitor: {
    ArrayExpression(path) {
      // [...[e1, e2]] => [e1, e2]
      path.node.elements = path.node.elements
        .map(el => {
          if (el && el.type == "SpreadElement" && el.argument.type == "ArrayExpression") {
            return el.argument.elements;
          } else {
            return [el];
          }
        })
        .flat();
    },

    ObjectExpression(path) {
      let props = path.node.properties;
      /// {...e} => e
      if (props.length == 1 && props[0].type == "SpreadElement") {
        path.replaceWith(props[0].argument);
      }
    },

    CallExpression(path) {
      let expr = path.node;
      if (
        expr.arguments.length == 0 &&
        expr.arguments.length == 0 &&
        expr.callee.type == "ArrowFunctionExpression" &&
        expr.callee.body.type == "BlockStatement" &&
        expr.callee.body.body.length == 1 &&
        expr.callee.body.body[0].type == "ReturnStatement" &&
        expr.callee.body.body[0].argument
      ) {
        // `(() => { return e; })()` => `e`
        path.replaceWith(expr.callee.body.body[0].argument);
        path.visit();
      } else {
        path.node.arguments = path.node.arguments
          .map(arg => {
            // f(...[x, y]) => f(x, y)
            if (arg.type == "SpreadElement" && arg.argument.type == "ArrayExpression") {
              return arg.argument.elements.map(el => el!);
            } else {
              return [arg];
            }
          })
          .flat();
      }
    },
  },
})
Example #6
Source File: Generator.test.ts    From mpflow with MIT License 5 votes vote down vote up
describe('Generator', () => {
  test('should extend package', async () => {
    const volume = new Volume()
    volume.fromJSON({
      '/package.json': '{"name": "test"}',
    })
    const fs = createFsFromVolume(volume)
    const generator = new Generator('/', { inputFileSystem: fs as any, outputFileSystem: fs as any })
    generator.extendPackage(
      {
        scripts: {
          test: 'echo "test"',
        },
        dependencies: {
          module: '^1.0.0',
        },
      },
      'test',
    )
    await generator.generate()
    expect(JSON.parse(fs.readFileSync('/package.json', 'utf8') as string)).toEqual({
      name: 'test',
      scripts: {
        test: 'echo "test"',
      },
      dependencies: {
        module: '^1.0.0',
      },
    })
  })

  test('should process file', async () => {
    const volume = new Volume()
    volume.fromJSON({
      '/modify.js': 'before modify',
      '/rename.js': 'before rename',
      '/transform.js': 'module.exports = "before transform";',
    })
    const fs = createFsFromVolume(volume)
    const generator = new Generator('/', { inputFileSystem: fs as any, outputFileSystem: fs as any })

    generator.processFile('test modify', 'modify.js', (fileInfo, api) => {
      api.replace('after modify')
    })
    generator.processFile('test rename', 'rename.js', (fileInfo, api) => {
      api.rename('rename.ts')
    })
    generator.processFile('test transform', 'transform.js', (fileInfo, api) => {
      api.transform(
        ({ types }: typeof babel): PluginObj => ({
          name: 'test transform',
          visitor: {
            StringLiteral(p) {
              p.node.value = 'after transform'
            },
          },
        }),
        {},
      )
    })

    await generator.generate(false)

    expect(volume.toJSON()).toEqual({
      '/modify.js': 'after modify',
      '/rename.ts': 'before rename',
      '/transform.js': 'module.exports = "after transform";',
    })
  })
})
Example #7
Source File: index.ts    From react-loosely-lazy with Apache License 2.0 4 votes vote down vote up
export default function ({
  types: t,
}: {
  types: typeof BabelTypes;
}): PluginObj {
  function getCallExpression(path: NodePath) {
    let maybeCallExpression = path.parentPath;

    if (
      maybeCallExpression.isMemberExpression() &&
      !maybeCallExpression.node.computed &&
      t.isIdentifier(maybeCallExpression.get('property'), { name: 'Map' })
    ) {
      maybeCallExpression = maybeCallExpression.parentPath;
    }

    return maybeCallExpression;
  }

  function getLazyArguments(
    callExpression: NodePath<BabelTypes.CallExpression>
  ): [NodePath<BabelTypes.Function>, NodePath<BabelTypes.ObjectExpression>] {
    const args = callExpression.get<'arguments'>('arguments');
    const loader = args[0];
    let options = args[1];

    if (!loader.isFunction()) {
      throw new Error('Loader argument must be a function');
    }

    if (!options || !options.isObjectExpression()) {
      callExpression.node.arguments.push(t.objectExpression([]));
      options = callExpression.get<'arguments'>('arguments')[1];

      return [loader, options as NodePath<BabelTypes.ObjectExpression>];
    }

    return [loader, options];
  }

  type PropertiesMap = Map<
    string,
    NodePath<BabelTypes.ObjectMethod | BabelTypes.ObjectProperty>
  >;

  function getPropertiesMap(
    options: NodePath<BabelTypes.ObjectExpression>
  ): PropertiesMap {
    const properties = options.get<'properties'>('properties');

    return properties.reduce<PropertiesMap>((map, property) => {
      if (property.isSpreadElement()) {
        throw new Error(
          'Options argument does not support SpreadElement as it is not statically analyzable'
        );
      }

      // @ts-expect-error TypeScript type narrowing does not work correctly here
      map.set(property.node.key.name, property);

      return map;
    }, new Map());
  }

  function getSSR(map: PropertiesMap, lazyMethodName: string): boolean {
    const property = map.get('ssr');
    if (!property) {
      // @ts-ignore This should be available
      return DEFAULT_OPTIONS[lazyMethodName].ssr;
    }

    if (property.isObjectMethod()) {
      throw new Error('Unable to statically analyze ssr option');
    }

    // @ts-expect-error TypeScript type narrowing does not work correctly here
    const value = property.node.value;
    if (!t.isBooleanLiteral(value)) {
      throw new Error('Unable to statically analyze ssr option');
    }

    return value.value;
  }

  type TransformLoaderOptions = {
    env: 'client' | 'server';
    loader: NodePath<BabelTypes.Function>;
    noopRedundantLoaders: boolean;
    ssr: boolean;
  };

  // TODO Remove the require hack when this library drops non-streaming support
  function transformLoader({
    env,
    loader,
    noopRedundantLoaders,
    ssr,
  }: TransformLoaderOptions): { importPath: string | void } {
    let importPath;

    loader.traverse({
      Import(nodePath: NodePath<BabelTypes.Import>) {
        const maybeImportCallExpression = nodePath.parentPath;
        if (!maybeImportCallExpression.isCallExpression()) {
          return;
        }

        // Get the import path when the parent is a CallExpression and its first
        // argument is a StringLiteral
        const maybeImportPath =
          maybeImportCallExpression.get<'arguments'>('arguments')[0];
        if (maybeImportPath.isStringLiteral()) {
          importPath = maybeImportPath.node.value;
        }

        // Do not transform the loader on the client
        if (env === 'client') {
          return;
        }

        // When on the server, either do nothing or replace the loader with a noop
        if (!ssr) {
          if (noopRedundantLoaders) {
            const noopComponent = t.arrowFunctionExpression(
              [],
              t.nullLiteral()
            );
            loader.replaceWith(t.arrowFunctionExpression([], noopComponent));
          }

          return;
        }

        // Replace the import with a require
        nodePath.replaceWith(t.identifier('require'));

        // Transform all then calls to be synchronous in order to support
        // named exports
        let maybeMemberExpression: NodePath<BabelTypes.Node> =
          nodePath.parentPath.parentPath;
        let previousIdOrExpression;
        while (maybeMemberExpression.isMemberExpression()) {
          const { property } = maybeMemberExpression.node;
          if (!t.isIdentifier(property, { name: 'then' })) {
            break;
          }

          const maybeCallExpression = maybeMemberExpression.parentPath;
          if (!maybeCallExpression.isCallExpression()) {
            break;
          }

          if (!previousIdOrExpression) {
            const loaderId = loader.scope.generateUidIdentifier();
            nodePath.scope.push({
              id: loaderId,
              init: maybeImportCallExpression.node,
            });
            previousIdOrExpression = loaderId;
          }

          const thenId = loader.scope.generateUidIdentifier();
          const thenArgs = maybeCallExpression.get<'arguments'>('arguments');
          const onFulfilled = thenArgs[0];
          if (onFulfilled.isExpression()) {
            nodePath.scope.push({
              id: thenId,
              init: onFulfilled.node,
            });
          }

          const replacement = t.callExpression(thenId, [
            previousIdOrExpression,
          ]);

          maybeCallExpression.replaceWith(replacement);

          maybeMemberExpression = maybeMemberExpression.parentPath.parentPath;
          previousIdOrExpression = replacement;
        }
      },
      AwaitExpression() {
        throw new Error('Loader argument does not support await expressions');
      },
      MemberExpression(nodePath: NodePath<BabelTypes.MemberExpression>) {
        const maybeCallExpression = nodePath.parentPath;
        if (
          t.isIdentifier(nodePath.node.property, { name: 'then' }) &&
          maybeCallExpression.isCallExpression()
        ) {
          const thenArgs = maybeCallExpression.get<'arguments'>('arguments');
          if (thenArgs.length > 1) {
            throw new Error(
              'Loader argument does not support Promise.prototype.then with more than one argument'
            );
          }
        }

        if (t.isIdentifier(nodePath.node.property, { name: 'catch' })) {
          throw new Error(
            'Loader argument does not support Promise.prototype.catch'
          );
        }
      },
    });

    return {
      importPath,
    };
  }

  function buildModuleIdProperty(opts: GetModulePathOptions) {
    return t.objectProperty(
      t.identifier(MODULE_ID_KEY),
      t.stringLiteral(getModulePath(opts))
    );
  }

  return {
    visitor: {
      ImportDeclaration(
        path: NodePath<BabelTypes.ImportDeclaration>,
        state: {
          opts?: BabelPluginOptions | undefined | false;
          filename?: string;
        }
      ) {
        const {
          client,
          modulePathReplacer,
          noopRedundantLoaders = true,
          resolverOptions = {},
        } = state.opts || {};
        const { filename } = state;
        const source = path.node.source.value;
        const customResolver = createCustomResolver(resolverOptions);

        if (source !== PACKAGE_NAME) {
          return;
        }

        const bindingNames = LAZY_METHODS;
        const bindings = bindingNames
          .map(name => path.scope.getBinding(name))
          .filter(isPresent);

        bindings.forEach(binding => {
          const lazyMethodName = binding.identifier.name;

          binding.referencePaths.forEach((refPath: NodePath) => {
            const callExpression = getCallExpression(refPath);
            if (!callExpression.isCallExpression()) {
              return;
            }

            const [loader, lazyOptions] = getLazyArguments(callExpression);
            const propertiesMap = getPropertiesMap(lazyOptions);

            const { importPath } = transformLoader({
              env: client ? 'client' : 'server',
              loader,
              noopRedundantLoaders,
              ssr: getSSR(propertiesMap, lazyMethodName),
            });

            if (!importPath) {
              return;
            }

            if (!filename) {
              throw new Error(
                `Babel transpilation target for ${importPath} not found`
              );
            }

            // Add the moduleId property to options
            lazyOptions.node.properties.push(
              buildModuleIdProperty({
                filename,
                importPath,
                modulePathReplacer,
                resolver: customResolver,
              })
            );
          });
        });
      },
    },
  };
}
Example #8
Source File: index.ts    From glaze with MIT License 4 votes vote down vote up
module.exports = function plugin({
  types: t,
}: {
  types: typeof types;
}): PluginObj<{
  hasSxProps: boolean | undefined;
  useStylingImportName: string;
  glazeImportDeclaration: types.ImportDeclaration | undefined;
  pathsToAddHook: Set<NodePath<types.Function>>;
}> {
  return {
    name: 'babel-plugin-glaze',
    visitor: {
      Program: {
        enter(path, state) {
          state.hasSxProps = false;
          state.useStylingImportName = 'useStyling';
          state.pathsToAddHook = new Set();

          const { node } = path;
          // Check if something from glaze is already imported
          const glazeImportDeclaration = node.body.find(
            (s) => t.isImportDeclaration(s) && s.source.value === 'glaze',
          );

          // Something is already imported from glaze
          if (t.isImportDeclaration(glazeImportDeclaration)) {
            state.glazeImportDeclaration = glazeImportDeclaration;
            // Check if it's `useStyling` which is imported
            const useStylingImport = glazeImportDeclaration.specifiers.find(
              (s) => t.isImportSpecifier(s) && s.imported.name === 'useStyling',
            );

            if (useStylingImport) {
              state.useStylingImportName = useStylingImport.local.name;
            }
          }
        },
        exit(path, { hasSxProps, glazeImportDeclaration }) {
          if (hasSxProps) {
            const importUseStyling = template.ast`import { useStyling } from 'glaze'`;

            // Something is already imported from glaze
            if (glazeImportDeclaration) {
              // Check if it's `useStyling` which is imported
              const useStylingImport = glazeImportDeclaration.specifiers.find(
                (s) =>
                  t.isImportSpecifier(s) && s.imported.name === 'useStyling',
              );

              // If it's not `useStyling`, we add it to the import
              if (!useStylingImport) {
                glazeImportDeclaration.specifiers.push(
                  t.importSpecifier(
                    t.identifier('useStyling'),
                    t.identifier('useStyling'),
                  ),
                );
              }
            }
            // Nothing imported yet from glaze
            else {
              path.unshiftContainer('body', importUseStyling);
            }
          }
        },
      },
      Function: {
        exit(path, state) {
          if (state.pathsToAddHook.has(path)) {
            const nodeToAddHook = path.node;
            const createUseStylingHook = template.statement.ast`
            const sx = ${state.useStylingImportName}();
           `;

            if (t.isBlockStatement(nodeToAddHook.body)) {
              /* Verify that the hook is not yet created.
                 If not,we create it
              */

              const block = nodeToAddHook.body;
              const isAlreadyImported = block.body.some(
                (st) =>
                  t.isVariableDeclaration(st) &&
                  st.declarations.find(
                    (decl) =>
                      t.isCallExpression(decl.init) &&
                      t.isIdentifier(decl.init.callee, {
                        name: state.useStylingImportName,
                      }),
                  ),
              );
              if (!isAlreadyImported) {
                nodeToAddHook.body.body.unshift(createUseStylingHook);
              }
            } else {
              /* Not a block statement. We first need to create one. Example:

                const Comp = () => <div sx={{color: "blue"}}>hello</div>

                Should become:

                const Comp = () => {
                  return <div sx={{color: "blue"}}>hello</div>
                }
              */
              nodeToAddHook.body = t.blockStatement([
                createUseStylingHook,
                t.returnStatement(nodeToAddHook.body),
              ]);
            }
          }
        },
      },
      JSXAttribute(path, state) {
        const { node } = path;

        if (t.isJSXIdentifier(node.name, { name: 'sx' })) {
          const jsxIdentifier = node.name;
          if (t.isJSXExpressionContainer(node.value)) {
            if (t.isExpression(node.value.expression)) {
              /* 1. We set this value so we know that somewhere in the file
                 the `sx` props is used.We will need therefore to import
                 `useStyling` hook from 'glaze'. This is done in the Program exit.
              */
              state.hasSxProps = true;

              /* 2. Find the nearest parent component (or the current scope)
                 and add it to a list that will be processed wihtin the
                 Function exit. This is where we will create de `sx` variable:
                 `const sx = useStyling()`
              */
              const pathsToAddHook =
                findNearestParentComponent(path) || path.scope.path;

              if (pathsToAddHook.isFunction()) {
                state.pathsToAddHook.add(pathsToAddHook);
              }

              /* 3. This is where we transform the `sx` props */
              if (t.isJSXOpeningElement(path.parent)) {
                const objectExpression = node.value.expression;

                // Remove the `sx` props
                path.remove();

                // Check if a className props already exists
                let classNameAttributeIdentifier = path.parent.attributes.find(
                  (a) =>
                    t.isJSXAttribute(a) &&
                    t.isJSXIdentifier(a.name, { name: 'className' }),
                );

                if (t.isJSXAttribute(classNameAttributeIdentifier)) {
                  // A className props already exists
                  const classNameNode = classNameAttributeIdentifier.value;
                  const baseTemplateLiteral = t.templateLiteral(
                    [
                      t.templateElement({ raw: '' }, false),
                      t.templateElement({ raw: '' }, true),
                    ],
                    [
                      t.callExpression(t.identifier(jsxIdentifier.name), [
                        objectExpression,
                      ]),
                    ],
                  );

                  /* Handle the case where className is currently a
                     an expression. E.g: `className={fn(...)}` or
                     `className={isValid ? "my-class" : ""}`
                  */
                  if (
                    t.isJSXExpressionContainer(classNameNode) &&
                    t.isExpression(classNameNode.expression)
                  ) {
                    baseTemplateLiteral.quasis.splice(
                      1,
                      0,
                      t.templateElement({ raw: ' ' }, false),
                    );
                    baseTemplateLiteral.expressions.unshift(
                      classNameNode.expression,
                    );
                  } else if (t.isStringLiteral(classNameNode)) {
                    /* Handle the case where the className is currently a string */
                    if (classNameNode.value !== '') {
                      baseTemplateLiteral.quasis[0] = t.templateElement(
                        { raw: `${classNameNode.value} ` },
                        false,
                      );
                    }
                  }
                  classNameAttributeIdentifier.value = t.jsxExpressionContainer(
                    baseTemplateLiteral,
                  );
                } else {
                  /* Handle the case where no className exists yet */
                  classNameAttributeIdentifier = t.jsxAttribute(
                    t.jsxIdentifier('className'),
                    t.jsxExpressionContainer(
                      t.callExpression(t.identifier(jsxIdentifier.name), [
                        objectExpression,
                      ]),
                    ),
                  );
                  path.parent.attributes.unshift(classNameAttributeIdentifier);
                }
              }
            }
          }
        }
      },
    },
  };
};
Example #9
Source File: index.ts    From vanilla-extract with MIT License 4 votes vote down vote up
export default function (): PluginObj<Context> {
  return {
    pre({ opts }) {
      if (!opts.filename) {
        // TODO Make error better
        throw new Error('Filename must be available');
      }

      this.isESM = false;
      this.isCssFile = cssFileFilter.test(opts.filename);
      this.alreadyCompiled = false;

      this.importIdentifiers = new Map();
      this.namespaceImport = '';

      const packageInfo = getPackageInfo(opts.cwd);

      if (!packageInfo.name) {
        throw new Error(
          `Closest package.json (${packageInfo.path}) must specify name`,
        );
      }
      this.packageName = packageInfo.name;
      // Encode windows file paths as posix
      this.filePath = posix.join(
        ...relative(packageInfo.dirname, opts.filename).split(sep),
      );
    },
    visitor: {
      Program: {
        exit(path) {
          if (this.isCssFile && !this.alreadyCompiled) {
            // Wrap module with file scope calls
            const buildSetFileScope = this.isESM
              ? buildSetFileScopeESM
              : buildSetFileScopeCJS;
            path.unshiftContainer(
              'body',
              buildSetFileScope({
                filePath: t.stringLiteral(this.filePath),
                packageName: t.stringLiteral(this.packageName),
              }),
            );

            path.pushContainer('body', buildEndFileScope());
          }
        },
      },
      ImportDeclaration(path) {
        this.isESM = true;
        if (!this.isCssFile || this.alreadyCompiled) {
          // Bail early if file isn't a .css.ts file or the file has already been compiled
          return;
        }

        if (path.node.source.value === filescopePackageIdentifier) {
          // If file scope import is found it means the file has already been compiled
          this.alreadyCompiled = true;
          return;
        } else if (packageIdentifiers.has(path.node.source.value)) {
          path.node.specifiers.forEach((specifier) => {
            if (t.isImportNamespaceSpecifier(specifier)) {
              this.namespaceImport = specifier.local.name;
            } else if (t.isImportSpecifier(specifier)) {
              const { imported, local } = specifier;

              const importName = (
                t.isIdentifier(imported) ? imported.name : imported.value
              ) as StyleFunction;

              if (styleFunctions.includes(importName)) {
                this.importIdentifiers.set(local.name, importName);
              }
            }
          });
        }
      },
      ExportDeclaration() {
        this.isESM = true;
      },
      CallExpression(path) {
        if (!this.isCssFile || this.alreadyCompiled) {
          // Bail early if file isn't a .css.ts file or the file has already been compiled
          return;
        }

        const { node } = path;

        if (
          t.isIdentifier(node.callee, { name: 'require' }) &&
          t.isStringLiteral(node.arguments[0], {
            value: filescopePackageIdentifier,
          })
        ) {
          // If file scope import is found it means the file has already been compiled
          this.alreadyCompiled = true;
          return;
        }

        const usedExport = getRelevantCall(
          node,
          this.namespaceImport,
          this.importIdentifiers,
        );

        if (usedExport && usedExport in debuggableFunctionConfig) {
          if (
            node.arguments.length <
            debuggableFunctionConfig[
              usedExport as keyof typeof debuggableFunctionConfig
            ].maxParams
          ) {
            const debugIdent = getDebugId(path);

            if (debugIdent) {
              node.arguments.push(t.stringLiteral(debugIdent));
            }
          }
        }
      },
    },
  };
}
Example #10
Source File: add-exports-array.ts    From mpflow with MIT License 4 votes vote down vote up
/**
 * 向配置文件如
 * module.exports = { plugins: [] }
 * 中的 plugins 添加插件信息
 */
export default function (api: typeof babel, options: Options): PluginObj {
  const { types: t, template } = api
  const { fieldName, items } = options

  let pluginsArrayExpression: NodePath<ArrayExpression> | null

  if (!fieldName || !items || !items.length)
    return {
      name: 'add-exports-array',
      visitor: {},
    }

  return {
    name: 'add-exports-array',
    pre() {
      pluginsArrayExpression = null
    },
    visitor: {
      AssignmentExpression(p) {
        // 寻找 module.exports = { plugins: [] }
        if (
          !m
            .assignmentExpression(
              '=',
              m.memberExpression(m.identifier('module'), m.identifier('exports')),
              m.objectExpression(),
            )
            .match(p.node)
        )
          return

        const objectExpression = p.get('right') as NodePath<ObjectExpression>
        const properties = objectExpression.get('properties')

        properties.forEach(property => {
          if (
            !m
              .objectProperty(m.or(m.stringLiteral(fieldName), m.identifier(fieldName)), m.arrayExpression())
              .match(property.node)
          )
            return

          pluginsArrayExpression = property.get('value') as NodePath<ArrayExpression>
        })
      },
      Program: {
        exit(p) {
          if (!pluginsArrayExpression) {
            // 如果找不到 module.exports = { plugins: [] }
            // 则在末尾加一句 exports.plugins = (exports.plugins || []).concat([])
            const statement = template.statement(`
              exports.FIELD_NAME = (exports.FIELD_NAME || []).concat([]);
            `)({
              FIELD_NAME: t.identifier(fieldName),
            })
            const [statementPath] = p.pushContainer('body', statement)
            pluginsArrayExpression = statementPath.get('expression.right.arguments.0') as NodePath<ArrayExpression>
          }
          const targetArray = pluginsArrayExpression
          // 添加 item
          items.forEach(item => {
            const [pluginName, option] = Array.isArray(item) ? item : [item]
            if (!option) {
              targetArray.pushContainer('elements', t.stringLiteral(pluginName))
            } else {
              targetArray.pushContainer(
                'elements',
                t.arrayExpression([t.stringLiteral(pluginName), template.expression(JSON.stringify(option))()]),
              )
            }
          })
        },
      },
    },
  }
}