diff --git a/.changeset/add-max-inline-size-option.md b/.changeset/add-max-inline-size-option.md new file mode 100644 index 000000000..df4dd34da --- /dev/null +++ b/.changeset/add-max-inline-size-option.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": minor +--- + +Add `maxInlineSize` option to assets loader for size-based asset inlining. Use it together with `inline: true` — assets whose largest variant is within the threshold are inlined as base64 URIs; larger assets are extracted as separate files. diff --git a/packages/repack/src/loaders/assetsLoader/assetsLoader.ts b/packages/repack/src/loaders/assetsLoader/assetsLoader.ts index 919ddf87e..2376a6334 100644 --- a/packages/repack/src/loaders/assetsLoader/assetsLoader.ts +++ b/packages/repack/src/loaders/assetsLoader/assetsLoader.ts @@ -222,8 +222,15 @@ export default async function repackAssetsLoader( }) ); + const largestVariantSize = Math.max( + ...assets.map((asset) => asset.data.length) + ); + const shouldInlineAsset = + !!options.inline && + (!options.maxInlineSize || largestVariantSize <= options.maxInlineSize); + let result: string; - if (options.inline) { + if (shouldInlineAsset) { logger.debug(`Inlining assets for request ${resourcePath}`); result = inlineAssets({ assets, resourcePath }); } else { diff --git a/packages/repack/src/loaders/assetsLoader/options.ts b/packages/repack/src/loaders/assetsLoader/options.ts index 6f7632771..3b9f4b66a 100644 --- a/packages/repack/src/loaders/assetsLoader/options.ts +++ b/packages/repack/src/loaders/assetsLoader/options.ts @@ -18,6 +18,7 @@ export interface AssetLoaderOptions { scalableAssetExtensions?: string[]; scalableAssetResolutions?: string[]; inline?: boolean; + maxInlineSize?: number; publicPath?: string; remote?: AssetLoaderRemoteOptions; } @@ -39,6 +40,7 @@ export const optionsSchema: Schema = { type: 'array', }, inline: { type: 'boolean' }, + maxInlineSize: { type: 'number' }, publicPath: { type: 'string' }, remote: { type: 'object', diff --git a/packages/repack/src/utils/__tests__/__snapshots__/getAssetTransformRules.test.ts.snap b/packages/repack/src/utils/__tests__/__snapshots__/getAssetTransformRules.test.ts.snap index 831420881..c99a6420e 100644 --- a/packages/repack/src/utils/__tests__/__snapshots__/getAssetTransformRules.test.ts.snap +++ b/packages/repack/src/utils/__tests__/__snapshots__/getAssetTransformRules.test.ts.snap @@ -8,6 +8,7 @@ exports[`getAssetTransformRules should add SVGR rule when svg="svgr" 1`] = ` "loader": "@callstack/repack/assets-loader", "options": { "inline": undefined, + "maxInlineSize": undefined, "remote": undefined, }, }, @@ -32,6 +33,7 @@ exports[`getAssetTransformRules should add URI rule when svg="uri" 1`] = ` "loader": "@callstack/repack/assets-loader", "options": { "inline": undefined, + "maxInlineSize": undefined, "remote": undefined, }, }, @@ -51,6 +53,7 @@ exports[`getAssetTransformRules should add XML rule when svg="xml" 1`] = ` "loader": "@callstack/repack/assets-loader", "options": { "inline": undefined, + "maxInlineSize": undefined, "remote": undefined, }, }, @@ -70,6 +73,7 @@ exports[`getAssetTransformRules should include additional options for SVGR 1`] = "loader": "@callstack/repack/assets-loader", "options": { "inline": undefined, + "maxInlineSize": undefined, "remote": undefined, }, }, @@ -95,6 +99,7 @@ exports[`getAssetTransformRules should return default asset transform rules when "loader": "@callstack/repack/assets-loader", "options": { "inline": undefined, + "maxInlineSize": undefined, "remote": undefined, }, }, @@ -110,6 +115,23 @@ exports[`getAssetTransformRules should return rules with inline option when prov "loader": "@callstack/repack/assets-loader", "options": { "inline": true, + "maxInlineSize": undefined, + "remote": undefined, + }, + }, + }, +] +`; + +exports[`getAssetTransformRules should return rules with maxInlineSize option when provided 1`] = ` +[ + { + "test": /\\\\\\.\\(bmp\\|gif\\|jpg\\|jpeg\\|png\\|psd\\|svg\\|webp\\|tiff\\|m4v\\|mov\\|mp4\\|mpeg\\|mpg\\|webm\\|aac\\|aiff\\|caf\\|m4a\\|mp3\\|wav\\|html\\|pdf\\|yaml\\|yml\\|otf\\|ttf\\|zip\\|obj\\)\\$/, + "use": { + "loader": "@callstack/repack/assets-loader", + "options": { + "inline": undefined, + "maxInlineSize": 1024, "remote": undefined, }, }, @@ -125,6 +147,7 @@ exports[`getAssetTransformRules should return rules with remote options when pro "loader": "@callstack/repack/assets-loader", "options": { "inline": undefined, + "maxInlineSize": undefined, "remote": { "enabled": true, "publicPath": "https://example.com/assets", diff --git a/packages/repack/src/utils/__tests__/getAssetTransformRules.test.ts b/packages/repack/src/utils/__tests__/getAssetTransformRules.test.ts index 0f4c53abb..63d20826c 100644 --- a/packages/repack/src/utils/__tests__/getAssetTransformRules.test.ts +++ b/packages/repack/src/utils/__tests__/getAssetTransformRules.test.ts @@ -14,6 +14,14 @@ describe('getAssetTransformRules', () => { expect(rules).toMatchSnapshot(); }); + it('should return rules with maxInlineSize option when provided', () => { + const rules = getAssetTransformRules({ maxInlineSize: 1024 }); + + // @ts-ignore + expect(rules[0]?.use?.options?.maxInlineSize).toEqual(1024); + expect(rules).toMatchSnapshot(); + }); + it('should return rules with remote options when provided', () => { const remoteOptions = { publicPath: 'https://example.com/assets' }; const rules = getAssetTransformRules({ remote: remoteOptions }); diff --git a/packages/repack/src/utils/getAssetTransformRules.ts b/packages/repack/src/utils/getAssetTransformRules.ts index efed8ca03..c3040121d 100644 --- a/packages/repack/src/utils/getAssetTransformRules.ts +++ b/packages/repack/src/utils/getAssetTransformRules.ts @@ -39,6 +39,13 @@ interface GetAssetTransformRulesOptions { */ inline?: boolean; + /** + * Maximum asset file size in bytes to inline as base64 URIs. + * Requires `inline: true`. Assets whose largest scale variant exceeds this + * threshold will be extracted as separate files instead of being inlined. + */ + maxInlineSize?: number; + /** * Configuration for remote asset loading. */ @@ -57,7 +64,8 @@ interface GetAssetTransformRulesOptions { * Creates `module.rules` configuration for handling assets in React Native applications. * * @param options Configuration options - * @param options.inline Whether to inline assets as base64 URIs (defaults to false) + * @param options.inline Whether to inline all assets as base64 URIs (defaults to false) + * @param options.maxInlineSize Maximum asset file size in bytes to inline as base64 URIs (requires inline: true); larger assets are extracted as separate files * @param options.remote Configuration for remote asset loading with publicPath and optional assetPath function * @param options.svg Determines how SVG files should be processed ('svgr', 'xml', or 'uri') * @@ -65,6 +73,7 @@ interface GetAssetTransformRulesOptions { */ export function getAssetTransformRules({ inline, + maxInlineSize, remote, svg, }: GetAssetTransformRulesOptions = {}) { @@ -85,7 +94,7 @@ export function getAssetTransformRules({ test: getAssetExtensionsRegExp(extensions), use: { loader: '@callstack/repack/assets-loader', - options: { inline, remote: remoteOptions }, + options: { inline, maxInlineSize, remote: remoteOptions }, }, }); diff --git a/tests/integration/src/loaders/__snapshots__/rspack/assets-loader.test.ts.snap b/tests/integration/src/loaders/__snapshots__/rspack/assets-loader.test.ts.snap index 7a4a7b268..d3b7d914b 100644 --- a/tests/integration/src/loaders/__snapshots__/rspack/assets-loader.test.ts.snap +++ b/tests/integration/src/loaders/__snapshots__/rspack/assets-loader.test.ts.snap @@ -418,3 +418,70 @@ exports[`assetLoader > should inline asset > without scales 2`] = ` └─ out/ └─ main.js" `; + +exports[`assetLoader > should inline asset based on maxInlineSize > extracts asset when size exceeds threshold 1`] = ` +{ + "__packager_asset": true, + "hash": "373411381c08b2c5034c814c24c26b19", + "height": 51, + "httpServerLocation": "assets/__fixtures__/assets", + "name": "logo", + "scales": [ + 1, + ], + "type": "png", + "width": 297, +} +`; + +exports[`assetLoader > should inline asset based on maxInlineSize > extracts asset when size exceeds threshold 2`] = ` +"/ +└─ out/ + ├─ drawable-mdpi/ + │ └─ __fixtures___assets_logo.png + └─ main.js" +`; + +exports[`assetLoader > should inline asset based on maxInlineSize > inlines asset when size is within threshold 1`] = ` +{ + "height": 51, + "scale": 1, + "uri": "data:image/png;base64,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", + "width": 297, +} +`; + +exports[`assetLoader > should inline asset based on maxInlineSize > inlines asset when size is within threshold 2`] = ` +"/ +└─ out/ + └─ main.js" +`; + +exports[`assetLoader > should inline asset based on maxInlineSize > uses largest variant to determine threshold (multi-scale asset) 1`] = ` +{ + "__packager_asset": true, + "hash": "86abef08a42e972a96c958d6fa9d43da,02ad731a8881911e488b51c48a4cc6c1,d9f7c31eebb3a41cc180431867b04f38", + "height": 272, + "httpServerLocation": "assets/__fixtures__/assets", + "name": "star", + "scales": [ + 1, + 2, + 3, + ], + "type": "png", + "width": 286, +} +`; + +exports[`assetLoader > should inline asset based on maxInlineSize > uses largest variant to determine threshold (multi-scale asset) 2`] = ` +"/ +└─ out/ + ├─ drawable-mdpi/ + │ └─ __fixtures___assets_star.png + ├─ drawable-xhdpi/ + │ └─ __fixtures___assets_star.png + ├─ drawable-xxhdpi/ + │ └─ __fixtures___assets_star.png + └─ main.js" +`; diff --git a/tests/integration/src/loaders/__snapshots__/webpack/assets-loader.test.ts.snap b/tests/integration/src/loaders/__snapshots__/webpack/assets-loader.test.ts.snap index 7a4a7b268..d3b7d914b 100644 --- a/tests/integration/src/loaders/__snapshots__/webpack/assets-loader.test.ts.snap +++ b/tests/integration/src/loaders/__snapshots__/webpack/assets-loader.test.ts.snap @@ -418,3 +418,70 @@ exports[`assetLoader > should inline asset > without scales 2`] = ` └─ out/ └─ main.js" `; + +exports[`assetLoader > should inline asset based on maxInlineSize > extracts asset when size exceeds threshold 1`] = ` +{ + "__packager_asset": true, + "hash": "373411381c08b2c5034c814c24c26b19", + "height": 51, + "httpServerLocation": "assets/__fixtures__/assets", + "name": "logo", + "scales": [ + 1, + ], + "type": "png", + "width": 297, +} +`; + +exports[`assetLoader > should inline asset based on maxInlineSize > extracts asset when size exceeds threshold 2`] = ` +"/ +└─ out/ + ├─ drawable-mdpi/ + │ └─ __fixtures___assets_logo.png + └─ main.js" +`; + +exports[`assetLoader > should inline asset based on maxInlineSize > inlines asset when size is within threshold 1`] = ` +{ + "height": 51, + "scale": 1, + "uri": "data:image/png;base64,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", + "width": 297, +} +`; + +exports[`assetLoader > should inline asset based on maxInlineSize > inlines asset when size is within threshold 2`] = ` +"/ +└─ out/ + └─ main.js" +`; + +exports[`assetLoader > should inline asset based on maxInlineSize > uses largest variant to determine threshold (multi-scale asset) 1`] = ` +{ + "__packager_asset": true, + "hash": "86abef08a42e972a96c958d6fa9d43da,02ad731a8881911e488b51c48a4cc6c1,d9f7c31eebb3a41cc180431867b04f38", + "height": 272, + "httpServerLocation": "assets/__fixtures__/assets", + "name": "star", + "scales": [ + 1, + 2, + 3, + ], + "type": "png", + "width": 286, +} +`; + +exports[`assetLoader > should inline asset based on maxInlineSize > uses largest variant to determine threshold (multi-scale asset) 2`] = ` +"/ +└─ out/ + ├─ drawable-mdpi/ + │ └─ __fixtures___assets_star.png + ├─ drawable-xhdpi/ + │ └─ __fixtures___assets_star.png + ├─ drawable-xxhdpi/ + │ └─ __fixtures___assets_star.png + └─ main.js" +`; diff --git a/tests/integration/src/loaders/assets-loader.test.ts b/tests/integration/src/loaders/assets-loader.test.ts index e6995732a..63c68c38d 100644 --- a/tests/integration/src/loaders/assets-loader.test.ts +++ b/tests/integration/src/loaders/assets-loader.test.ts @@ -25,7 +25,8 @@ async function compileBundle( resourceExtensionType: string; }) => string; publicPath: string; - } + }, + maxInlineSize?: number ) { const virtualPlugin = await createVirtualModulePlugin(virtualModules); @@ -49,6 +50,7 @@ async function compileBundle( platform, inline, remote, + maxInlineSize, }, }, }, @@ -147,6 +149,71 @@ describe('assetLoader', () => { }); }); + describe('should inline asset based on maxInlineSize', () => { + it('inlines asset when size is within threshold', async () => { + // logo.android.png is 1948 bytes — threshold above it triggers inline + const { code, volume } = await compileBundle( + 'android', + { + ...getReactNativeVirtualModules(), + './index.js': + "export { default } from './__fixtures__/assets/logo.png';", + }, + true, + undefined, + 2000 + ); + + const context: { Export?: { default: Record } } = {}; + vm.runInNewContext(code, context); + + expect(context.Export?.default).toMatchSnapshot(); + expect(volume.toTree()).toMatchSnapshot(); + }); + + it('extracts asset when size exceeds threshold', async () => { + // logo.android.png is 1948 bytes — threshold below it prevents inline + const { code, volume } = await compileBundle( + 'android', + { + ...getReactNativeVirtualModules(), + './index.js': + "export { default } from './__fixtures__/assets/logo.png';", + }, + true, + undefined, + 1000 + ); + + const context: { Export?: { default: Record } } = {}; + vm.runInNewContext(code, context); + + expect(context.Export?.default).toMatchSnapshot(); + expect(volume.toTree()).toMatchSnapshot(); + }); + + it('uses largest variant to determine threshold (multi-scale asset)', async () => { + // star@3x.png is 21176 bytes — the largest variant determines whether to inline + const { code, volume } = await compileBundle( + 'android', + { + ...getReactNativeVirtualModules(), + './index.js': + "export { default } from './__fixtures__/assets/star.png';", + }, + true, + undefined, + 10000 // below star@3x (21176 bytes), so should extract + ); + + const context: { Export?: { default: Record } } = {}; + vm.runInNewContext(code, context); + + expect(context.Export?.default).toMatchSnapshot(); + expect(volume.toTree()).toMatchSnapshot(); + }); + }); + describe('should convert to remote-asset', () => { it('without scales', async () => { const { code, volume } = await compileBundle( diff --git a/website/src/latest/api/loaders/assets-loader.md b/website/src/latest/api/loaders/assets-loader.md index 45ad74945..85a64e7a0 100644 --- a/website/src/latest/api/loaders/assets-loader.md +++ b/website/src/latest/api/loaders/assets-loader.md @@ -33,6 +33,7 @@ interface AssetsLoaderOptions { scalableAssetResolutions?: string[]; devServerEnabled?: boolean; inline?: boolean; + maxInlineSize?: number; publicPath?: string; remote?: { enabled: boolean; @@ -81,6 +82,19 @@ Whether development server is enabled. By default, this option is determined by When true, assets will be inlined as base64 in the JS bundle instead of being extracted to separate files. +### maxInlineSize + +- Type: `number` +- Default: `undefined` + +File size threshold in bytes used together with `inline: true`. Assets whose largest scale variant is smaller than or equal to this value will be inlined; larger assets will be extracted as separate files. Has no effect when `inline` is not `true`. + +The threshold is compared against the **largest scale variant** (e.g. `@3x`), not the `@1x` file — when an asset is inlined, all scale variants are embedded into the bundle. + +:::tip +Learn more about size-based inlining in the [Inlining Assets guide](/docs/guides/inline-assets#size-based-inlining). +::: + ### publicPath - Type: `string` diff --git a/website/src/latest/api/utils/get-asset-transform-rules.md b/website/src/latest/api/utils/get-asset-transform-rules.md index fb110388f..5210df214 100644 --- a/website/src/latest/api/utils/get-asset-transform-rules.md +++ b/website/src/latest/api/utils/get-asset-transform-rules.md @@ -11,6 +11,7 @@ This helper function allows you to create a single configuration for all assets ```ts interface GetAssetTransformRulesOptions { inline?: boolean; + maxInlineSize?: number; remote?: { publicPath: string; assetPath?: (args: { @@ -38,6 +39,16 @@ Whether to inline assets as base64 URIs. Learn more about the inlining assets in the [Inlining Assets guide](/docs/guides/inline-assets). ::: +### options.maxInlineSize + +- Type: `number` + +File size threshold in bytes used together with `inline: true`. Assets whose largest scale variant is smaller than or equal to this value will be inlined; larger assets will be extracted as separate files. Has no effect when `inline` is not `true`. + +:::tip +Learn more about size-based inlining in the [Inlining Assets guide](/docs/guides/inline-assets#size-based-inlining). +::: + ### options.remote - Type: `object` diff --git a/website/src/latest/docs/guides/inline-assets.md b/website/src/latest/docs/guides/inline-assets.md index 87f9b6286..7e35f0ea9 100644 --- a/website/src/latest/docs/guides/inline-assets.md +++ b/website/src/latest/docs/guides/inline-assets.md @@ -61,13 +61,55 @@ import image from './image.png'; The value of `image` in this example would be either an object with `uri`, `width`, `height` and `scale` or an array of such objects, in case there are multiple scales. -## Selective inlining +## Size-based inlining + +You can combine `inline: true` with `maxInlineSize` to set a file size threshold in bytes. Assets whose **largest scale variant** is smaller than or equal to the threshold will be inlined; larger assets will be extracted as separate files. + +```js title="rspack.config.cjs" +const Repack = require("@callstack/repack"); + +module.exports = { + module: { + rules: [ + { + test: Repack.getAssetExtensionsRegExp(), + use: { + loader: "@callstack/repack/assets-loader", + options: { inline: true, maxInlineSize: 20 * 1024 }, // inline assets up to 20 KB + }, + }, + ], + }, +}; +``` + +Or via the helper: + +```js title="rspack.config.cjs" +const Repack = require("@callstack/repack"); + +module.exports = { + module: { + rules: [...Repack.getAssetTransformRules({ inline: true, maxInlineSize: 20 * 1024 })], + }, +}; +``` + +:::info Scale variants and the size threshold + +The threshold is compared against the **largest scale variant** of the asset (e.g. `@3x`), not the `@1x` file. This is intentional — when an asset is inlined, all scale variants are embedded into the bundle, so the largest one is what determines the worst-case size impact. + +For example, a `@1x` PNG that is 10 KB may have a `@3x` variant of 80 KB. With `inline: true` and `maxInlineSize: 20 * 1024` the asset would be extracted, not inlined, because the `@3x` variant exceeds the threshold. + +::: + +## Selective inlining by path You can provide multiple rules with Re.Pack's [Assets loader](/api/loaders/assets-loader) - one rule would extract the assets and another would inline them. There's no limit how many of these rules you could have. Make sure you configure those rules not to overlap, so that any single asset is only processed by one rule (by one [Assets loader](/api/loaders/assets-loader)). Use combination of `include`, `exclude` and `test` (for extensions matching) to configure each rule. -```js title="rspack.config.mjs" +```js title="rspack.config.cjs" const Repack = require("@callstack/repack"); module.exports = {