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 17, 2021
1 parent 0bbc062 commit 28a9009
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 12 deletions.
89 changes: 89 additions & 0 deletions packages/cli-build/src/inspect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {Stats, StatsCompilation} from 'webpack';
import {flatMap, uniqBy, sumBy} from 'lodash';
import chalk from 'chalk';
import prettyBytes from 'pretty-bytes';
import {BuildInspectSettings, RuleConfig, Severity} from '@reskript/settings';

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

const print = (config: RuleConfig<any>, message: string) => {
const severity = typeof config === 'string' ? config : config[0];
console.log(`${SEVERITY_PREFIX[severity]} ${message}`);
};

const extractInitialChunks = (stats: Stats) => {
const {children = []} = stats.toJson('normal');
const chunks = uniqBy(flatMap(children, child => child.chunks ?? []), chunk => chunk.id);
const initialChunks = chunks.filter(chunk => chunk.initial);
return initialChunks;
};

const isRequired = (rule: RuleConfig<any>): boolean => rule !== 'off' && rule[0] !== 'off';

const isRequiredWithConfig = <T>(rule: RuleConfig<T>): rule is [Severity, T] => {
if (typeof rule === 'string') {
return false;
}

return rule[0] !== 'off';
};

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 (stats: Stats, settings: BuildInspectSettings) => {
const initialChunks = extractInitialChunks(stats);
const results: Severity[] = [];

if (isRequired(settings.initialResources.count)) {
print(
settings.initialResources.count,
`Initial resource count: ${initialChunks.length}`
);
}
if (isRequired(settings.initialResources.totalSize)) {
print(
settings.initialResources.totalSize,
`Initial resource size: ${prettyBytes(sumBy(initialChunks, chunk => chunk.size))} (not gzipped)`
);
}
if (isRequiredWithConfig(settings.initialResources.disallowImports)) {
const [severity, config] = settings.initialResources.disallowImports;
const unwantedChunkImports = findDisallowedImportsInChunks(initialChunks, config);
if (unwantedChunkImports.length) {
for (const {file, moduleName} of unwantedChunkImports) {
print(
settings.initialResources.disallowImports,
`Initial chunk ${file} includes disallowed module ${moduleName}`
);
}
}
results.push(severity);
}

if (results.includes('error')) {
process.exit(10);
}
};
9 changes: 1 addition & 8 deletions packages/cli-build/src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import * as path from 'path';
import ConsoleTable, {Column} from 'tty-table';
import chalk, {ForegroundColor} from 'chalk';
import {Stats} from 'webpack';
import {isEmpty, difference, flatMap, uniqBy, sortBy, max, sumBy} from 'lodash';
import prettyBytes from 'pretty-bytes';
import {isEmpty, difference, flatMap, uniqBy, sortBy, max} from 'lodash';
import {ProjectSettings} from '@reskript/settings';
import {WebpackCompileAsset} from './interface';

Expand Down Expand Up @@ -93,12 +92,6 @@ export const drawBuildReport = (stats: Stats[]): void => {
for (const {color, name, size, indicator} of templateSegments) {
console.log(chalk[color](`${name.padStart(maxNameLength)} ${size.padEnd(maxSizeLength)} ${indicator}`));
}

const initialSegments = templateSegments.filter(segment => segment.indicator === 'initial');
const initialLoadingSize = sumBy(initialSegments, segment => segment.sizeInBytes);
console.log('');
console.log(`Total initial requests: ${initialChunks.size}`);
console.log(`Total initial size: ${prettyBytes(initialLoadingSize)} (not gzipped)`);
};

export interface WebpackResult {
Expand Down
11 changes: 7 additions & 4 deletions packages/cli-build/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import {
checkProjectSettings,
BuildContext,
} from '@reskript/config-webpack';
import {readProjectSettings, BuildEnv} from '@reskript/settings';
import {readProjectSettings, BuildEnv, ProjectSettings} from '@reskript/settings';
import * as partials from './partial';
import {BuildCommandLineArgs} from './interface';
import {drawFeatureMatrix, drawBuildReport, printWebpackResult, WebpackResult} from './report';
import inspect from './inspect';

const build = (configuration: Configuration | Configuration[]): Promise<Stats> => {
const executor = (resolve: (value: Stats) => void) => webpack(
Expand Down Expand Up @@ -51,8 +52,7 @@ const build = (configuration: Configuration | Configuration[]): Promise<Stats> =
return new Promise(executor);
};

const createConfigurations = (cmd: BuildCommandLineArgs): Configuration[] => {
const projectSettings = readProjectSettings(cmd, 'build');
const createConfigurations = (cmd: BuildCommandLineArgs, projectSettings: ProjectSettings): Configuration[] => {
const featureNames = difference(Object.keys(projectSettings.featureMatrix), projectSettings.build.excludeFeatures);

if (cmd.featureOnly && !featureNames.includes(cmd.featureOnly)) {
Expand Down Expand Up @@ -102,7 +102,8 @@ export default async (cmd: BuildCommandLineArgs): Promise<void> => {
rimraf.sync(path.join(cmd.cwd, 'dist'));
}

const [initial, ...configurations] = createConfigurations(cmd);
const projectSettings = readProjectSettings(cmd, 'build');
const [initial, ...configurations] = createConfigurations(cmd, projectSettings);

if (!initial) {
const error = 'No build configuration created, you are possibly providing a feature matrix with dev only';
Expand All @@ -114,4 +115,6 @@ export default async (cmd: BuildCommandLineArgs): Promise<void> => {
const initialStats = await build([initial]);
const stats = !!configurations.length && await build(configurations);
drawBuildReport(stats ? [initialStats, stats] : [initialStats]);
console.log('');
inspect(initialStats, projectSettings.build.inspect);
};
10 changes: 10 additions & 0 deletions packages/settings/src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ const fillBuildSettings = (settings?: PartialBuildSettings, cwd: string = proces
modules: true,
...settings?.style,
},
inspect: {
...settings?.inspect,
initialResources: {
count: 'print',
totalSize: 'print',
sizeDeviation: 'off',
disallowImports: 'off',
...settings?.inspect?.initialResources,
},
},
};
};

Expand Down
16 changes: 16 additions & 0 deletions packages/settings/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ export interface BuildScriptSettings {
readonly finalize: (babelConfig: TransformOptions, env: BuildEntry) => TransformOptions;
}

export type Severity = 'off' | 'print' | 'warning' | 'error';

export type RuleConfig<T> = 'off' | 'print' | [Severity, T];

export interface BuildInspectInitialResource {
readonly count: RuleConfig<number>;
readonly totalSize: RuleConfig<number>;
readonly sizeDeviation: RuleConfig<number>;
readonly disallowImports: RuleConfig<string[]>;
}

export interface BuildInspectSettings {
initialResources: BuildInspectInitialResource;
}

export interface BuildSettings {
// 产出的资源路径前缀
readonly publicPath?: string;
Expand All @@ -53,6 +68,7 @@ export interface BuildSettings {
readonly script: BuildScriptSettings;
// 最终手动处理webpack配置
readonly finalize: (webpackConfig: WebpackConfiguration, env: BuildEntry) => WebpackConfiguration;
readonly inspect: BuildInspectSettings;
}

export interface DevServerSettings {
Expand Down
38 changes: 38 additions & 0 deletions packages/settings/src/validate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import {validate} from 'schema-utils';

const ruleConfig = (valueSchema: any) => {
return {
anyOf: [
{
type: 'string',
enum: ['off', 'print'],
},
{
type: 'array',
items: [
{
type: 'string',
enum: ['off', 'print', 'warning', 'error'],
},
valueSchema,
],
additionalItems: false,
},
],
};
};

// `schema`并不是一个完全符合JSON Schema的东西
const schema: any = {
properties: {
Expand Down Expand Up @@ -84,6 +106,22 @@ const schema: any = {
},
additionalProperties: false,
},
inspect: {
type: 'object',
properties: {
initialResources: {
type: 'object',
properties: {
count: ruleConfig({type: 'number'}),
totalSize: ruleConfig({type: 'number'}),
sizeDeviation: ruleConfig({type: 'number'}),
disallowImports: ruleConfig({type: 'array', items: {type: 'string'}}),
},
additionalProperties: false,
},
},
additionalProperties: false,
},
},
additionalProperties: false,
type: 'object',
Expand Down

0 comments on commit 28a9009

Please sign in to comment.