自定义扩展规则

功能介绍

考虑到用户会有特有的规则定义需求,Rsdoctor 除了内部已有的规则外,还提供了外部接口供用户定制自己的规则检查。外部扩展接口通过 extends 字段配置到 Rsdoctor 插件上,配置也放在 rules 字段内。

  • 示例如下:
// src/rules/assets-count-limit.ts
import { defineRule } from '@rsdoctor/core/rules';

export const AssetsCountLimit = defineRule(() => ({
  meta: {
    category: 'bundle',
    severity: 'Warn',
    title: 'assets-count-limit',
    defaultConfig: {
      limit: 10,
    },
  },
  check({ chunkGraph, report, ruleConfig }) {
    const assets = chunkGraph.getAssets();

    if (assets.length > ruleConfig.limit) {
      report({
        message: 'The count of assets is bigger than limit',
        detail: {
          type: 'link',
          link: 'https://rsdoctor.dev/zh/guide/start/quick-start', // This link just for show case.
        },
      });
    }
  },
}));
// rsbuild.config.ts
import { AssetsCountLimit } from './rules/assets-count-limit';

export default {
  tools: {
    bundlerChain: (chain) => {
      chain.plugin('Rsdoctor').use(RsdoctorRspackPlugin, [
        {
          linter: {
            level: 'Error',
            extends: [AssetsCountLimit],
            rules: {
              'assets-count-limit': [
                'on',
                {
                  limit: 1, // rule custom configs
                },
              ],
            },
          },
        },
      ]);
    },
  },
};

可按照下面自定义规则详细步骤进行规则定义及编写。

自定义规则步骤

1. 安装

编写自定义规则时,除了安装基本的 @rsdoctor/rspack-plugin(@rsdoctor/webpack-plugin) 依赖,还需要安装 @rsdoctor/core 并使用 @rsdoctor/core/rules 中的 defineRule 函数来定义统一的 Rsdoctor 规则。

npm
yarn
pnpm
bun
npm add @rsdoctor/core -D

2. 编写规则

编写规则需要先使用defineRule函数,它内部输入一个函数,此函数返回一个固定格式的对象。参考按照如下实例:

// src/rules/some-rule.ts
import { defineRule } from '@rsdoctor/core/rules';

const ruleTitle = 'check-rule';
const ruleConfig = {
  // some rule configs
};

export const CheckRule = defineRule<typeof ruleTitle, config>(() => ({
  meta: {
    category: 'bundle', // rule category
    severity: 'Warn', // rule severity
    title: ruleTitle, // rule title
    defaultConfig: {
      // rule default config
    },
  },
  check(ruleContext) {
    // rule check...
  },
}));

其中,meta 字段为此规则的固定配置和内容,check 字段为包含规则检查具体逻辑的回调,它们的类型如下。

meta 对象

meta 类型定义查看:RuleMeta

属性含义
  • meta
    • category
      • info: 定义规则分类:编译规则或构建打包规则。
      • type: 'compile' | 'bundle'。
    • title
      • info:规则标题,用于在 Rsdoctor 报告页面中展示。
      • type:string | 泛型,用户可通过泛型下传。
    • severity
      • info: 规则等级。
      • type: 参考下方 ErrorLevel 类型。
      • default: 'Warn'
    • defaultConfig
      • info:规则默认配置。自定义规则中可能需要特有的配置,defaultConfig 可用于配置默认的规则配置。
      • type:泛型,用户可通过泛型定义。如上面示例。
    • referenceUrl
      • info: 规则文档链接。
      • type: string。

check 函数

check 函数主要是用来做规则判断的,参数 ruleContext 是 Rsdoctor 在构建分析过程中所整合到的所有构建信息,类型定义如下。 可以在 check 函数的函数体内利用构建信息进行自定义的规则判断。判断后,如果规则检查出问题,可通过参数中的 report 方法进行上报,具体参见下一步。

CheckCallback 类型
type CheckCallback<Config = DefaultRuleConfig> = (
  context: RuleCheckerContext<Config>,
) => void | Promise<void>;

RuleCheckerContext 类型定义请查看详情

示例

如下实例是对 assets 资源的个数进行一个限制的自定义规则:

// src/rules/some-rule.ts
const CheckRule = defineRule<typeof ruleTitle, config>(() => ({
  // .....

  check({ chunkGraph, report, ruleConfig }) {
    const assets = chunkGraph.getAssets();

    if (assets.length > ruleConfig.limit) {
      report({
        message: 'The count of assets is bigger than limit',
        detail: {
          type: 'link',
          link: 'https://rsdoctor.dev/zh/guide/start/quick-start', // This link just for show case.
        },
      });
    }
  },
}));

3. 上报规则结果

上报错误需要使用check 回调函数的参数中的 report 方法,report 方法的参数主要是包含以下的几个部分:

  • message:错误消息
  • document:文件数据, 用于描述报错代码文件位置及代码位置。
  • suggestions:规则建议。
  • detail:详细信息,主要是提供给前端的额外数据。

详细类型定义查看:ReportData

4. 规则结果展示

report 函数会把自定义规则的错误信息传递给 compilation 的 errors 或 warnings,会在构建的时候在终端中输出构建规则结果提示,甚至中断构建。 同时,Rsdoctor 还有两种组件可以作为规则展示,详细的查看下方展示组件

  • 基本规则警示组件
  • 代码展示组件

展示组件

基本规则警示组件

  • 组件类型

    LinkRule Type

  • 组件输入

    • type

      • 使用组件类型。
      • value: 'link'。
    • title

      • 规则标题。
      • type: string。
    • description

      • 规则描述。数据来源为 report 中的 message 或 detail.description:
        report({
          message: 'The count of assets is bigger than limit',
          detail: {
            // ......
            description: 'The count of assets is bigger than limit',
          },
        });
      • type: string。
    • level

      • 规则级别。
      • type:warn | error。
    • link:

      • 规则详情。数据来源为 detail.link:
        report({
          detail: {
            // ......
            link: 'http://....',
          },
        });
      • type:string。
  • 示例

report({
  message: 'The count of assets is bigger than limit',
  detail: {
    type: 'link',
    link: 'https://rsdoctor.dev/zh/guide/start/quick-start', // This link just for show case.
  },
});
  • 组件展示

代码展示组件

  • 组件类型

    CodeViewRule Type

  • 组件输入

    • type

      • 使用组件类型。
      • value: 'code-view'。
    • title

      • 规则标题。
      • type: string。
    • description

      • 规则描述。数据来源为 report 中的 message 或 detail.description:
        report({
          message: 'The count of assets is bigger than limit',
          detail: {
            // ......
            description: 'The count of assets is bigger than limit',
          },
        });
      • type: string。
    • level

      • 规则级别。
      • type:warn | error。
    • file

      • 代码详情展示。
      • type
        • file: string, 代码文件地址。
        • content: string, 代码内容。
        • ranges: SourceRange, 代码行列范围。
  • 示例

const detail {
    type: 'code-view',
    file: {
      path: document.path,
      content: document.content,
      ranges: [node.loc!],
    },
  };

  report({
    message,
    document,
    detail,
  });

类型定义

RuleMeta

interface RuleMeta<
  Config = DefaultRuleConfig,
  Title extends DefaultRuleTitle = DefaultRuleTitle,
> {
  title: Title;
  category:
  severity: ErrorLevel;
  referenceUrl?: string;
  defaultConfig?: Config;
}

/** Error Level */
export enum ErrorLevel {
  Ignore = 0,
  Warn = 1,
  Error = 2,
}

RuleCheckerContext

interface RuleCheckerContext<Config> {
  /** 工程根目录 */
  root: string;
  /** 当前规则配置 */
  ruleConfig: Config;
  /** 工程配置 */
  configs: ConfigData;
  /** 构建错误 */
  errors: Error[];
  /** Chunk 图 */
  chunkGraph: ChunkGraph;
  /** 模块图 */
  moduleGraph: ModuleGraph;
  /** 依赖图 */
  packageGraph: PackageGraph;
  /** loader 数据 */
  loader: LoaderData;
  /**
   * 上报错误
   * @param {any} error - 错误数据
   * @param {any} replacer - 替换原错误
   */
  report(error: ReportData, replacer?: any): void;
}

ReportData

interface ReportData {
  /** 错误消息 */
  message: string;
  /** 文件数据 */
  document?: ReportDocument;
  /** 诊断建议 */
  suggestions?: Suggestion;
  /**
   * 详细信息
   *   - 主要是提供给前端的额外数据
   */
  detail?: any;
}

/** Error file information */
interface ReportDocument {
  /** file path */
  path: string;
  /**  Is it a transformed code */
  isTransformed?: boolean;
  /** code content */
  content: string;
  range: Range;
}

LinkRuleStoreData

interface BaseRuleStoreData extends Pick<RuleMessage, 'code' | 'category'> {
  /**
   * unique of error
   */
  id: number | string;
  /**
   * title of alerts
   */
  title: string;
  /**
   * description of alerts
   */
  description?: string;
  /**
   * level of error
   */
  level: 'warn' | 'error';
  /**
   * rule doc link
   */
  link?: string;
}

interface LinkRuleStoreData extends BaseRuleStoreData {
  type: 'link';
}

CodeViewRule

interface CodeViewRuleStoreData extends BaseRuleStoreData {
  type: 'code-view';
  file: {
    /**
     * file path
     */
    path: string;
    /**
     * the code content
     */
    content: string;
    /**
     * fix highlight range in source
     */
    ranges?: SourceRange[];
  };
}

/** Source code location */
interface SourcePosition {
  line?: number;
  column?: number;
  index?: number;
}

/** Source code range */
interface SourceRange {
  start: SourcePosition;
  end?: SourcePosition;
}

工具

AST 处理

在进行规则检测分析时,对模块进行AST分析等操作是常见的。为了提供更多辅助功能,我们还提供了 @rsdoctor/utils 包中的 @rsdoctor/utils/rule-utils,其中包含了许多实用的工具函数和方法。

/** 这里包含了 AST 所有节点的类型定义 */
export type Node = /* SyntaxNode */;

export interface parser {
  /** AST 迭代器 */
  walk,
  /**
   * 编译代码
   *   - 输出根节点为`Node.Program`
   */
  parse,
  /**
   * 编译接下来的首个表达式
   *   - 输出根节点为`Node.ExpressionStatement`
   */
  parseExpressionAt,
  /** 断言方法集 */
  asserts,
  /** 辅助方法集 */
  utils,
}

/** 文档类 */
export interface Document {
  /** 获取文件位置在文本中的位移 */
  positionAt!: (offset: number) => Position | undefined;
  /** 获取位移点在文件的位置 */
  offsetAt!: (position: Position) => number | undefined;
}

其中的 asserts 断言方法集,提供了所有 AST 节点的类型断言方法,utils 辅助方法集里提供了诸如判断某些语句的语义是否相同,获取 Import 节点等等常用的方法。

上报代码位置

有些错误需要提供代码的位置,所以需要提供document字段的内容。但是在这里有个很重要的区别,那就是每个模块实际上有两套代码,transformed 和 source,意为经过 loader 之后的代码和用户的原始代码,AST 实际上也是转换之后的代码格式。 为了给用户展示的方便,我们需要尽量使用原始代码,所以就需要用户在选中对应的 AST 节点之后,使用模块带有的 SourceMap 模块将位置信息转换到原始代码,如果模块因为某些特殊原因没有原始代码或者是 SourceMap,此时再使用转换后的代码/位置是比较合适的。一个比较典型的流程是这样的:

const module: SDK.ModuleInstance;
const node: Node.ImportDeclaration;
/** 默认类型为可选,实际上都是有值的 */
const transformedRange = node.loc!;
/** 若是模块的 SourceMap 不可用,则这个值为空 */
const sourceRange = module.getSourceRange(transformedRange);
/** 获取代码 */
const source = mod.getSource();

// 根据是否生成原始位置来判断使用哪个值
const range = (sourceRange ?? transformed) as Linter.Range;
const content = sourceRange ? source.source : source.transformed;

report({
  document: {
    path: module.path,
    content,
    range,
  },
});

数据上报

请跳转到数据上报进行查看