Skip to content

Commit

Permalink
feat(build): 支持构建产物中初始资源的全部检查规则 (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
otakustay committed Mar 18, 2021
1 parent 573254f commit 0a5b825
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 94 deletions.
89 changes: 0 additions & 89 deletions packages/cli-build/src/inspect.ts

This file was deleted.

14 changes: 14 additions & 0 deletions packages/cli-build/src/inspect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Stats} from 'webpack';
import {BuildInspectSettings} from '@reskript/settings';
import {run} from './utils';
import initialResources from './initialResources';


export default (stats: Stats, settings: BuildInspectSettings) => {
const {children = []} = stats.toJson('normal');
const processors = [
...initialResources(children, settings.initialResources),
];
run(processors);
};

83 changes: 83 additions & 0 deletions packages/cli-build/src/inspect/initialResources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {StatsCompilation} from 'webpack';
import {flatMap, uniqBy, sumBy, meanBy} from 'lodash';
import prettyBytes from 'pretty-bytes';
import {BuildInspectInitialResource} from '@reskript/settings';
import {RuleProcessor} from './utils';

const extractInitialChunks = (compilations: StatsCompilation[]) => {
const chunks = uniqBy(flatMap(compilations, child => child.chunks ?? []), chunk => chunk.id);
const initialChunks = chunks.filter(chunk => chunk.initial);
return initialChunks;
};

type StatsChunk = Exclude<StatsCompilation['chunks'], undefined>[0];

const findDisallowedImportsInChunks = (chunks: StatsChunk[], imports: string[]) => {
const matchImportInChunks = (disallowed: string) => {
const match = `node_modules/${disallowed}/`;
const matchedChunks = chunks.filter(chunk => chunk.modules?.some(m => m.nameForCondition?.includes(match)));
const toChunkMatch = (chunk: StatsChunk) => {
const file = chunk.files?.[0] ?? '(unknown)';
return {
file,
moduleName: disallowed,
};
};
return matchedChunks.map(toChunkMatch);
};

return flatMap(imports, matchImportInChunks);
};

export default (compilations: StatsCompilation[], settings: BuildInspectInitialResource) => {
const initialChunks = extractInitialChunks(compilations);

const count: RuleProcessor<number> = {
config: settings.count,
defaultConfigValue: Infinity,
check: (max, {notice, report}) => {
notice(`Initial resource count: ${initialChunks.length}`);
if (initialChunks.length > max) {
report(`Too many initial resoures, max allowed is ${max}`);
}
return initialChunks.length <= max;
},
};
const totalSize: RuleProcessor<number> = {
config: settings.totalSize,
defaultConfigValue: Infinity,
check: (max, {notice, report}) => {
const totalSize = sumBy(initialChunks, chunk => chunk.size);
notice(`Initial resource size: ${prettyBytes(totalSize)} (not gzipped)`);
if (totalSize > max) {
report(`Initial size is too large, max allowed is is ${prettyBytes(max)}`);
}
return totalSize <= max;
},
};
const sizeDeviation: RuleProcessor<number> = {
config: settings.sizeDeviation,
defaultConfigValue: Infinity,
check: (max, {report}) => {
const average = meanBy(initialChunks, chunk => chunk.size);
const abnormalChunks = initialChunks.filter(chunk => (chunk.size - average) / average > max);
for (const chunk of abnormalChunks) {
report(`Resource ${chunk.files?.[0]} has unbalanced size to other resources`);
}
return !abnormalChunks.length;
},
};
const disallowImports: RuleProcessor<string[]> = {
config: settings.disallowImports,
defaultConfigValue: [],
check: (disallowImports, {report}) => {
const unwantedChunkImports = findDisallowedImportsInChunks(initialChunks, disallowImports);
for (const {file, moduleName} of unwantedChunkImports) {
report(`Initial chunk ${file} includes disallowed module ${moduleName}`);
}
return !unwantedChunkImports.length;
},
};

return [count, totalSize, sizeDeviation, disallowImports];
};
63 changes: 63 additions & 0 deletions packages/cli-build/src/inspect/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import chalk from 'chalk';
import {RuleConfig, Severity} from '@reskript/settings';

const SEVERITY_PREFIX: Record<Severity, string> = {
'off': ' ',
'print': chalk.bgWhite.black(' I '),
'warn': chalk.bgYellow.white(' W '),
'error': chalk.bgRed.white(' E '),
};

export const createPrint = (severity: Severity) => (message: string) => {
console.log(`${SEVERITY_PREFIX[severity]} ${message}`);
};

export const normalizeRuleConfig = <T>(config: RuleConfig<T>, defaultConfigValue: T): [Severity, T] => {
if (typeof config === 'string') {
return [config, defaultConfigValue];
}

return config;
};

export interface CheckHelper {
notice: (message: string) => void;
report: (message: string) => void;
}

export type Check<T> = (configValue: T, helpers: CheckHelper) => boolean;

export interface RuleProcessor<T> {
config: RuleConfig<T>;
defaultConfigValue: T;
check: Check<T>;
}

export const run = (processors: Array<RuleProcessor<any>>): void => {
const results = processors.reduce(
(results, processor) => {
const [severity, configValue] = normalizeRuleConfig(processor.config, processor.defaultConfigValue);

if (severity === 'off') {
return results;
}

const helpers = {
report: createPrint(severity),
notice: createPrint('print'),
};
const result = processor.check(configValue, helpers);

if (!result) {
results.add(severity);
}

return results;
},
new Set<Severity>()
);

if (results.has('error')) {
process.exit(10);
}
};
2 changes: 1 addition & 1 deletion packages/settings/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface BuildInspectInitialResource {
readonly count: RuleConfig<number>;
// 初始加载的资源总大小,配置值为最大允许的体积,以字节为单位
readonly totalSize: RuleConfig<number>;
// 初始加载的各资源之间的体积差异,配置值为体积的标准差,超过该值即报告
// 初始加载的各资源之间的体积差异,配置值为单个资源的尺寸与所有资源尺寸平均值的差异系数,如0.3指尺寸必须在平均值的0.7-1.3倍之间
readonly sizeDeviation: RuleConfig<number>;
// 禁止在初始加载资源中包含某些第三方依赖,配置值为依赖名称的数组
readonly disallowImports: RuleConfig<string[]>;
Expand Down
2 changes: 1 addition & 1 deletion packages/settings/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const ruleConfig = (valueSchema: any) => {
items: [
{
type: 'string',
enum: ['off', 'print', 'warning', 'error'],
enum: ['off', 'print', 'warn', 'error'],
},
valueSchema,
],
Expand Down
4 changes: 1 addition & 3 deletions site/docs/settings/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ interface BuildInspectInitialResource {
readonly count: RuleConfig<number>;
// 初始加载的资源总大小,配置值为最大允许的体积,以字节为单位
readonly totalSize: RuleConfig<number>;
// 初始加载的各资源之间的体积差异,配置值为体积的标准差,超过该值即报告
// 初始加载的各资源之间的体积差异,配置值为单个资源的尺寸与所有资源尺寸平均值的差异系数,如0.3指尺寸必须在平均值的0.7-1.3倍之间
readonly sizeDeviation: RuleConfig<number>;
// 禁止在初始加载资源中包含某些第三方依赖,配置值为依赖名称的数组
readonly disallowImports: RuleConfig<string[]>;
Expand Down Expand Up @@ -431,5 +431,3 @@ exports.build = {
},
};
```

**注意:当前还不支持`sizeDeviation`的检查,同时并不支持`count``totalSize`的阈值检查。**

0 comments on commit 0a5b825

Please sign in to comment.