diff --git a/.changeset/rare-wolves-tease.md b/.changeset/rare-wolves-tease.md new file mode 100644 index 000000000000..b38d89c4f92b --- /dev/null +++ b/.changeset/rare-wolves-tease.md @@ -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): 支持基于文件体积来内联资源 diff --git a/packages/builder/builder-rspack-provider/tests/plugins/__snapshots__/default.test.ts.snap b/packages/builder/builder-rspack-provider/tests/plugins/__snapshots__/default.test.ts.snap index 12943f070ef8..b59a7a8095ab 100644 --- a/packages/builder/builder-rspack-provider/tests/plugins/__snapshots__/default.test.ts.snap +++ b/packages/builder/builder-rspack-provider/tests/plugins/__snapshots__/default.test.ts.snap @@ -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": { diff --git a/packages/builder/builder-shared/src/mergeBuilderConfig.ts b/packages/builder/builder-shared/src/mergeBuilderConfig.ts index 985851638333..0594f3503f00 100644 --- a/packages/builder/builder-shared/src/mergeBuilderConfig.ts +++ b/packages/builder/builder-shared/src/mergeBuilderConfig.ts @@ -1,4 +1,5 @@ import _ from '@modern-js/utils/lodash'; +import { isOverriddenConfigKey } from '@modern-js/utils'; export const mergeBuilderConfig = (...configs: T[]): T => _.mergeWith( @@ -11,8 +12,8 @@ export const mergeBuilderConfig = (...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; } diff --git a/packages/builder/builder-shared/src/plugins/InlineChunkHtmlPlugin.ts b/packages/builder/builder-shared/src/plugins/InlineChunkHtmlPlugin.ts index 3dc4e00d4485..9a1ebbdc512e 100644 --- a/packages/builder/builder-shared/src/plugins/InlineChunkHtmlPlugin.ts +++ b/packages/builder/builder-shared/src/plugins/InlineChunkHtmlPlugin.ts @@ -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; @@ -24,7 +31,9 @@ export type InlineChunkHtmlPluginOptions = { export class InlineChunkHtmlPlugin { name: string; - tests: RegExp[]; + styleTests: InlineChunkTest[]; + + scriptTests: InlineChunkTest[]; distPath: InlineChunkHtmlPluginOptions['distPath']; @@ -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; @@ -80,6 +90,16 @@ 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, @@ -87,21 +107,25 @@ export class InlineChunkHtmlPlugin { ) { 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', @@ -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; } @@ -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({ diff --git a/packages/builder/builder-shared/src/plugins/index.ts b/packages/builder/builder-shared/src/plugins/index.ts index c567132257f9..daba5ecab32f 100644 --- a/packages/builder/builder-shared/src/plugins/index.ts +++ b/packages/builder/builder-shared/src/plugins/index.ts @@ -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'; diff --git a/packages/builder/builder-shared/src/schema/output.ts b/packages/builder/builder-shared/src/schema/output.ts index ac68a282097a..a61a37c09e51 100644 --- a/packages/builder/builder-shared/src/schema/output.ts +++ b/packages/builder/builder-shared/src/schema/output.ts @@ -77,6 +77,12 @@ export const SvgDefaultExportSchema: ZodType = z.enum([ 'url', ]); +const inlineSchema = z.union([ + z.boolean(), + z.instanceof(RegExp), + z.anyFunction(), +]); + export const sharedOutputConfigSchema = z.partialObj({ distPath: DistPathConfigSchema, filename: FilenameConfigSchema, @@ -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()), diff --git a/packages/builder/builder-shared/src/types/config/output.ts b/packages/builder/builder-shared/src/types/config/output.ts index 4c5b422b855c..b059ed49b7d8 100644 --- a/packages/builder/builder-shared/src/types/config/output.ts +++ b/packages/builder/builder-shared/src/types/config/output.ts @@ -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'; @@ -261,11 +262,11 @@ export interface SharedOutputConfig { /** * Whether to inline output scripts files (.js files) into HTML with `