自定义扩展规则
功能介绍
考虑到用户会有特有的规则定义需求,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 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
-
title
-
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
-
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
-
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
-
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,
},
});
数据上报
请跳转到数据上报进行查看