Skip to content

Commit

Permalink
feat(builder): support inline assets by file size (#4563)
Browse files Browse the repository at this point in the history
* feat(builder): support inline assets by file size

* docs: fix
  • Loading branch information
chenjiahan committed Sep 1, 2023
1 parent 48fca89 commit c8b448b
Show file tree
Hide file tree
Showing 18 changed files with 288 additions and 51 deletions.
10 changes: 10 additions & 0 deletions .changeset/rare-wolves-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@modern-js/builder-shared': patch
'@modern-js/builder': patch
'@modern-js/utils': patch
'@modern-js/core': patch
---

feat(builder): support inline assets by file size

feat(builder): 支持基于文件体积来内联资源
Original file line number Diff line number Diff line change
Expand Up @@ -1493,9 +1493,10 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when prod
"htmlPlugin": [Function],
"inlinedAssets": Set {},
"name": "InlineChunkHtmlPlugin",
"tests": [
"scriptTests": [
/builder-runtime\\(\\[\\.\\]\\.\\+\\)\\?\\\\\\.js\\$/,
],
"styleTests": [],
},
],
"resolve": {
Expand Down
5 changes: 3 additions & 2 deletions packages/builder/builder-shared/src/mergeBuilderConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import _ from '@modern-js/utils/lodash';
import { isOverriddenConfigKey } from '@modern-js/utils';

export const mergeBuilderConfig = <T>(...configs: T[]): T =>
_.mergeWith(
Expand All @@ -11,8 +12,8 @@ export const mergeBuilderConfig = <T>(...configs: T[]): T =>
return undefined;
}

// always use source override target, if target defined.
if (['removeConsole'].includes(key)) {
// Some keys should use source to override target
if (isOverriddenConfigKey(key)) {
return source ?? target;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@
* modified from https://github.com/facebook/create-react-app/blob/master/packages/react-dev-utils/InlineChunkHtmlPlugin.js
*/
import { join } from 'path';
import { isString } from '@modern-js/utils';
import { isFunction, isString } from '@modern-js/utils';
import { addTrailingSlash } from '../utils';
import type { Compiler, Compilation } from 'webpack';
import type HtmlWebpackPlugin from 'html-webpack-plugin';
import type { HtmlTagObject } from 'html-webpack-plugin';
import { COMPILATION_PROCESS_STAGE, getPublicPathFromCompiler } from './util';

export type InlineChunkTestFunction = (params: {
size: number;
name: string;
}) => boolean;
export type InlineChunkTest = RegExp | InlineChunkTestFunction;

export type InlineChunkHtmlPluginOptions = {
tests: RegExp[];
styleTests: InlineChunkTest[];
scriptTests: InlineChunkTest[];
distPath: {
js?: string;
css?: string;
Expand All @@ -24,7 +31,9 @@ export type InlineChunkHtmlPluginOptions = {
export class InlineChunkHtmlPlugin {
name: string;

tests: RegExp[];
styleTests: InlineChunkTest[];

scriptTests: InlineChunkTest[];

distPath: InlineChunkHtmlPluginOptions['distPath'];

Expand All @@ -34,10 +43,11 @@ export class InlineChunkHtmlPlugin {

constructor(
htmlPlugin: typeof HtmlWebpackPlugin,
{ tests, distPath }: InlineChunkHtmlPluginOptions,
{ styleTests, scriptTests, distPath }: InlineChunkHtmlPluginOptions,
) {
this.name = 'InlineChunkHtmlPlugin';
this.tests = tests;
this.styleTests = styleTests;
this.scriptTests = scriptTests;
this.distPath = distPath;
this.inlinedAssets = new Set();
this.htmlPlugin = htmlPlugin;
Expand Down Expand Up @@ -80,28 +90,42 @@ export class InlineChunkHtmlPlugin {
return source;
}

matchTests(name: string, source: string, tests: InlineChunkTest[]) {
return tests.some(test => {
if (isFunction(test)) {
const size = source.length;
return test({ name, size });
}
return test.exec(name);
});
}

getInlinedScriptTag(
publicPath: string,
tag: HtmlTagObject,
compilation: Compilation,
) {
const { assets } = compilation;

// No need to inline scripts with src attribute
if (!(tag?.attributes.src && isString(tag.attributes.src))) {
return tag;
}

const { src, ...otherAttrs } = tag.attributes;
const scriptName = publicPath ? src.replace(publicPath, '') : src;

if (!this.tests.some(test => test.exec(scriptName))) {
return tag;
}
// If asset is not found, skip it
const asset = assets[scriptName];
if (asset == null) {
return tag;
}

const source = asset.source().toString();
const shouldInline = this.matchTests(scriptName, source, this.scriptTests);
if (!shouldInline) {
return tag;
}

const ret = {
tagName: 'script',
Expand Down Expand Up @@ -130,6 +154,7 @@ export class InlineChunkHtmlPlugin {
) {
const { assets } = compilation;

// No need to inline styles with href attribute
if (!(tag.attributes.href && isString(tag.attributes.href))) {
return tag;
}
Expand All @@ -138,13 +163,18 @@ export class InlineChunkHtmlPlugin {
? tag.attributes.href.replace(publicPath, '')
: tag.attributes.href;

if (!this.tests.some(test => test.exec(linkName))) {
// If asset is not found, skip it
const asset = assets[linkName];
if (asset == null) {
return tag;
}

const asset = assets[linkName];

const source = asset.source().toString();
const shouldInline = this.matchTests(linkName, source, this.styleTests);
if (!shouldInline) {
return tag;
}

const ret = {
tagName: 'style',
innerHTML: this.updateSourceMappingURL({
Expand Down
5 changes: 4 additions & 1 deletion packages/builder/builder-shared/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ export { HtmlCrossOriginPlugin } from './HtmlCrossOriginPlugin';
export { HtmlNoncePlugin } from './HtmlNoncePlugin';
export { HtmlAppIconPlugin } from './HtmlAppIconPlugin';
export { HtmlFaviconUrlPlugin, type FaviconUrls } from './HtmlFaviconUrlPlugin';
export { InlineChunkHtmlPlugin } from './InlineChunkHtmlPlugin';
export {
InlineChunkHtmlPlugin,
type InlineChunkTest,
} from './InlineChunkHtmlPlugin';
export { AssetsRetryPlugin } from './AssetsRetryPlugin';
export { CheckSyntaxPlugin } from './CheckSyntaxPlugin';
export { HtmlNetworkPerformancePlugin } from './HtmlNetworkPerformancePlugin';
Expand Down
10 changes: 8 additions & 2 deletions packages/builder/builder-shared/src/schema/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ export const SvgDefaultExportSchema: ZodType<SvgDefaultExport> = z.enum([
'url',
]);

const inlineSchema = z.union([
z.boolean(),
z.instanceof(RegExp),
z.anyFunction(),
]);

export const sharedOutputConfigSchema = z.partialObj({
distPath: DistPathConfigSchema,
filename: FilenameConfigSchema,
Expand All @@ -100,8 +106,8 @@ export const sharedOutputConfigSchema = z.partialObj({
enableAssetFallback: z.boolean(),
enableLatestDecorators: z.boolean(),
enableCssModuleTSDeclaration: z.boolean(),
enableInlineScripts: z.union([z.boolean(), z.instanceof(RegExp)]),
enableInlineStyles: z.union([z.boolean(), z.instanceof(RegExp)]),
enableInlineScripts: inlineSchema,
enableInlineStyles: inlineSchema,
externals: z.any(),
overrideBrowserslist: z.union([
z.array(z.string()),
Expand Down
9 changes: 5 additions & 4 deletions packages/builder/builder-shared/src/types/config/output.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { InlineChunkTest } from '../../plugins/InlineChunkHtmlPlugin';
import type { BuilderTarget } from '../builder';
import type { CrossOrigin } from './html';
import type { Externals } from 'webpack';
Expand Down Expand Up @@ -261,11 +262,11 @@ export interface SharedOutputConfig {
/**
* Whether to inline output scripts files (.js files) into HTML with `<script>` tags.
*/
enableInlineScripts?: boolean | RegExp;
enableInlineScripts?: boolean | InlineChunkTest;
/**
* Whether to inline output style files (.css files) into html with `<style>` tags.
*/
enableInlineStyles?: boolean | RegExp;
enableInlineStyles?: boolean | InlineChunkTest;
/**
* Specifies the range of target browsers that the project is compatible with.
* This value will be used by [@babel/preset-env](https://babeljs.io/docs/en/babel-preset-env) and
Expand Down Expand Up @@ -300,8 +301,8 @@ export interface NormalizedSharedOutputConfig extends SharedOutputConfig {
enableAssetFallback: boolean;
enableLatestDecorators: boolean;
enableCssModuleTSDeclaration: boolean;
enableInlineScripts: boolean | RegExp;
enableInlineStyles: boolean | RegExp;
enableInlineScripts: boolean | InlineChunkTest;
enableInlineStyles: boolean | InlineChunkTest;
svgDefaultExport: SvgDefaultExport;
cssModules: {
exportLocalsConvention: CssModuleLocalsConvention;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1833,9 +1833,10 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when produ
"htmlPlugin": [Function],
"inlinedAssets": Set {},
"name": "InlineChunkHtmlPlugin",
"tests": [
"scriptTests": [
/builder-runtime\\(\\[\\.\\]\\.\\+\\)\\?\\\\\\.js\\$/,
],
"styleTests": [],
},
MiniCssExtractPlugin {
"_sortedModulesCache": WeakMap {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ exports[`plugins/inlineChunk > should add InlineChunkHtmlPlugin properly by defa
"htmlPlugin": [Function],
"inlinedAssets": Set {},
"name": "InlineChunkHtmlPlugin",
"tests": [
"scriptTests": [
/builder-runtime\\(\\[\\.\\]\\.\\+\\)\\?\\\\\\.js\\$/,
],
"styleTests": [],
},
],
}
Expand Down Expand Up @@ -106,7 +107,8 @@ exports[`plugins/inlineChunk > should use proper plugin options when disableInli
"htmlPlugin": [Function],
"inlinedAssets": Set {},
"name": "InlineChunkHtmlPlugin",
"tests": [],
"scriptTests": [],
"styleTests": [],
},
],
}
Expand Down Expand Up @@ -161,10 +163,11 @@ exports[`plugins/inlineChunk > should use proper plugin options when enableInlin
"htmlPlugin": [Function],
"inlinedAssets": Set {},
"name": "InlineChunkHtmlPlugin",
"tests": [
/\\\\\\.js\\$/,
"scriptTests": [
/\\\\\\.\\(js\\|mjs\\|cjs\\|jsx\\)\\$/,
/builder-runtime\\(\\[\\.\\]\\.\\+\\)\\?\\\\\\.js\\$/,
],
"styleTests": [],
},
],
}
Expand Down Expand Up @@ -219,10 +222,12 @@ exports[`plugins/inlineChunk > should use proper plugin options when enableInlin
"htmlPlugin": [Function],
"inlinedAssets": Set {},
"name": "InlineChunkHtmlPlugin",
"tests": [
/\\\\\\.css\\$/,
"scriptTests": [
/builder-runtime\\(\\[\\.\\]\\.\\+\\)\\?\\\\\\.js\\$/,
],
"styleTests": [
/\\\\\\.css\\$/,
],
},
],
}
Expand Down
19 changes: 12 additions & 7 deletions packages/builder/builder/src/plugins/inlineChunk.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {
pick,
JS_REGEX,
CSS_REGEX,
RUNTIME_CHUNK_NAME,
isHtmlDisabled,
DefaultBuilderPlugin,
type InlineChunkTest,
} from '@modern-js/builder-shared';

export const builderPluginInlineChunk = (): DefaultBuilderPlugin => ({
Expand All @@ -28,22 +31,23 @@ export const builderPluginInlineChunk = (): DefaultBuilderPlugin => ({
enableInlineScripts,
} = config.output;

const tests: RegExp[] = [];
const scriptTests: InlineChunkTest[] = [];
const styleTests: InlineChunkTest[] = [];

if (enableInlineScripts) {
tests.push(
enableInlineScripts === true ? /\.js$/ : enableInlineScripts,
scriptTests.push(
enableInlineScripts === true ? JS_REGEX : enableInlineScripts,
);
}

if (enableInlineStyles) {
tests.push(
enableInlineStyles === true ? /\.css$/ : enableInlineStyles,
styleTests.push(
enableInlineStyles === true ? CSS_REGEX : enableInlineStyles,
);
}

if (!disableInlineRuntimeChunk) {
tests.push(
scriptTests.push(
// RegExp like /builder-runtime([.].+)?\.js$/
// matches builder-runtime.js and builder-runtime.123456.js
new RegExp(`${RUNTIME_CHUNK_NAME}([.].+)?\\.js$`),
Expand All @@ -53,7 +57,8 @@ export const builderPluginInlineChunk = (): DefaultBuilderPlugin => ({
chain.plugin(CHAIN_ID.PLUGIN.INLINE_HTML).use(InlineChunkHtmlPlugin, [
HtmlPlugin,
{
tests,
styleTests,
scriptTests,
distPath: pick(config.output.distPath, ['js', 'css']),
},
]);
Expand Down
9 changes: 7 additions & 2 deletions packages/cli/core/src/utils/mergeConfig.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { ensureArray } from '@modern-js/utils';
import { ensureArray, isOverriddenConfigKey } from '@modern-js/utils';
import { mergeWith, isFunction } from '@modern-js/utils/lodash';
import { UserConfig, NormalizedConfig } from '../types';

export const mergeConfig = <ExtendConfig extends Record<string, any>>(
configs: Array<UserConfig<ExtendConfig> | NormalizedConfig<ExtendConfig>>,
): NormalizedConfig<ExtendConfig> =>
mergeWith({}, ...configs, (target: any, source: any, key: string) => {
// Do not use the following merge logic for source.designSystem and tools.tailwind(css)
// Do not use the following merge logic for some keys
if (
key === 'designSystem' ||
(key === 'tailwindcss' && typeof source === 'object')
) {
return mergeWith({}, target ?? {}, source ?? {});
}

// Some keys should use source to override target
if (isOverriddenConfigKey(key)) {
return source ?? target;
}

if (Array.isArray(target) || Array.isArray(source)) {
if (target === undefined) {
return source;
Expand Down
Loading

0 comments on commit c8b448b

Please sign in to comment.