diff --git a/e2e/cases/emit-assets/index.test.ts b/e2e/cases/emit-assets/index.test.ts index f7a6ef9b93..8b8e25b08e 100644 --- a/e2e/cases/emit-assets/index.test.ts +++ b/e2e/cases/emit-assets/index.test.ts @@ -1,6 +1,10 @@ import { build } from '@e2e/helper'; import { expect, test } from '@playwright/test'; +function isIncludeFile(filenames: string[], includeFilename: string) { + return filenames.some((filename) => filename.includes(includeFilename)); +} + test('should allow to disable emit assets for node target', async () => { const rsbuild = await build({ cwd: __dirname, @@ -9,15 +13,24 @@ test('should allow to disable emit assets for node target', async () => { const files = await rsbuild.unwrapOutputJSON(); const filenames = Object.keys(files); + expect(isIncludeFile(filenames, 'dist/static/image/icon.png')).toBeTruthy(); + expect( - filenames.some((filename) => - filename.includes('dist/static/image/icon.png'), - ), - ).toBeTruthy(); + isIncludeFile(filenames, 'dist/server/static/image/icon.png'), + ).toBeFalsy(); +}); + +test('should allow to disable emit assets for json assets', async () => { + const rsbuild = await build({ + cwd: __dirname, + }); + + const files = await rsbuild.unwrapOutputJSON(); + const filenames = Object.keys(files); + + expect(isIncludeFile(filenames, 'dist/static/assets/test.json')).toBeTruthy(); expect( - filenames.some((filename) => - filename.includes('dist/server/static/image/icon.png'), - ), + isIncludeFile(filenames, 'dist/server/static/assets/test.json'), ).toBeFalsy(); }); diff --git a/e2e/cases/emit-assets/src/assets/test.json b/e2e/cases/emit-assets/src/assets/test.json new file mode 100644 index 0000000000..8d6b85c7b3 --- /dev/null +++ b/e2e/cases/emit-assets/src/assets/test.json @@ -0,0 +1,3 @@ +{ + "a": 1 +} diff --git a/e2e/cases/emit-assets/src/index.js b/e2e/cases/emit-assets/src/index.js index fba1a3e9d1..1890fa7296 100644 --- a/e2e/cases/emit-assets/src/index.js +++ b/e2e/cases/emit-assets/src/index.js @@ -1,3 +1,5 @@ import small from '../../../assets/icon.png?url'; +const testJson = new URL('./assets/test.json', import.meta.url).href; console.log(small); +console.log(testJson); diff --git a/packages/compat/webpack/tests/__snapshots__/default.test.ts.snap b/packages/compat/webpack/tests/__snapshots__/default.test.ts.snap index 8b9a70a4cf..9feb53e111 100644 --- a/packages/compat/webpack/tests/__snapshots__/default.test.ts.snap +++ b/packages/compat/webpack/tests/__snapshots__/default.test.ts.snap @@ -227,6 +227,7 @@ exports[`applyDefaultPlugins > should apply default plugins correctly 1`] = ` }, }, "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "static/js/async/[name].js", "devtoolModuleFilenameTemplate": [Function], "filename": "static/js/[name].js", @@ -592,6 +593,7 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when produ }, }, "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "static/js/async/[name].[contenthash:8].js", "filename": "static/js/[name].[contenthash:8].js", "hashFunction": "xxhash64", @@ -956,6 +958,7 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when targe "splitChunks": false, }, "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "[name].js", "filename": "[name].js", "hashFunction": "xxhash64", @@ -1252,6 +1255,7 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when targe "splitChunks": false, }, "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "static/js/async/[name].[contenthash:8].js", "filename": "static/js/[name].[contenthash:8].js", "hashFunction": "xxhash64", diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 8c19e779a2..05d736387e 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -4,6 +4,7 @@ import type { WatchOptions } from 'chokidar'; import color from 'picocolors'; import RspackChain from 'rspack-chain'; import { + ASSETS_DIST_DIR, CSS_DIST_DIR, DEFAULT_ASSET_PREFIX, DEFAULT_DATA_URL_SIZE, @@ -141,6 +142,7 @@ const getDefaultOutputConfig = (): NormalizedOutputConfig => ({ wasm: WASM_DIST_DIR, image: IMAGE_DIST_DIR, media: MEDIA_DIST_DIR, + assets: ASSETS_DIST_DIR, }, // Temporary placeholder, default: `${server.base}` assetPrefix: DEFAULT_ASSET_PREFIX, diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 6a781ee6d3..29a2fca7f9 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -11,6 +11,7 @@ export const FONT_DIST_DIR = 'static/font'; export const WASM_DIST_DIR = 'static/wasm'; export const IMAGE_DIST_DIR = 'static/image'; export const MEDIA_DIST_DIR = 'static/media'; +export const ASSETS_DIST_DIR = 'static/assets'; export const LOADER_PATH: string = join(__dirname); export const STATIC_PATH: string = join(__dirname, '../static'); export const COMPILED_PATH: string = join(__dirname, '../compiled'); diff --git a/packages/core/src/helpers/index.ts b/packages/core/src/helpers/index.ts index 3ba9804ac3..28c4c0c605 100644 --- a/packages/core/src/helpers/index.ts +++ b/packages/core/src/helpers/index.ts @@ -255,6 +255,8 @@ export function getFilename( return filename.image ?? `[name]${hash}[ext]`; case 'media': return filename.media ?? `[name]${hash}[ext]`; + case 'assets': + return filename.assets ?? `[name]${hash}[ext]`; default: throw new Error(`unknown key ${type} in "output.filename"`); } diff --git a/packages/core/src/plugins/asset.ts b/packages/core/src/plugins/asset.ts index 5e8d8d0d11..9585886007 100644 --- a/packages/core/src/plugins/asset.ts +++ b/packages/core/src/plugins/asset.ts @@ -76,14 +76,20 @@ export const pluginAsset = (): RsbuildPlugin => ({ api.modifyBundlerChain((chain, { isProd, environment }) => { const { config } = environment; + const getMergedFilename = ( + assetType: 'svg' | 'font' | 'image' | 'media' | 'assets', + ) => { + const distDir = config.output.distPath[assetType]; + const filename = getFilename(config, assetType, isProd); + return path.posix.join(distDir, filename); + }; + const createAssetRule = ( - assetType: 'image' | 'media' | 'font' | 'svg', + assetType: 'svg' | 'font' | 'image' | 'media', exts: string[], emit: boolean, ) => { const regExp = getRegExpForExts(exts); - const distDir = config.output.distPath[assetType]; - const filename = getFilename(config, assetType, isProd); const { dataUriLimit } = config.output; const maxSize = typeof dataUriLimit === 'number' @@ -95,21 +101,31 @@ export const pluginAsset = (): RsbuildPlugin => ({ emit, rule, maxSize, - filename: path.posix.join(distDir, filename), + filename: getMergedFilename(assetType), assetType, }); }; const { emitAssets } = config.output; + // image createAssetRule('image', IMAGE_EXTENSIONS, emitAssets); + // svg createAssetRule('svg', ['svg'], emitAssets); + // media createAssetRule( 'media', [...VIDEO_EXTENSIONS, ...AUDIO_EXTENSIONS], emitAssets, ); + // font createAssetRule('font', FONT_EXTENSIONS, emitAssets); + // assets + const assetsFilename = getMergedFilename('assets'); + chain.output.assetModuleFilename(assetsFilename); + if (!emitAssets) { + chain.module.generator.merge({ 'asset/resource': { emit: false } }); + } }); }, }); diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index bcdf0ee6f7..c2d47a7bc8 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -652,6 +652,11 @@ export type DistPathConfig = { * @default 'static/media' */ media?: string; + /** + * The output directory of assets, except for above (image, svg, font, html, wasm...) + * @default 'static/assets' + */ + assets?: string; }; export type FilenameConfig = { @@ -694,6 +699,11 @@ export type FilenameConfig = { * @default '[name].[contenthash:8][ext]' */ media?: string; + /** + * the name of other assets, except for above (image, svg, font, html, wasm...) + * @default '[name].[contenthash:8][ext]' + */ + assets?: string; }; export type DataUriLimit = { diff --git a/packages/core/tests/__snapshots__/asset.test.ts.snap b/packages/core/tests/__snapshots__/asset.test.ts.snap index a006134923..f21f706635 100644 --- a/packages/core/tests/__snapshots__/asset.test.ts.snap +++ b/packages/core/tests/__snapshots__/asset.test.ts.snap @@ -114,6 +114,9 @@ exports[`plugin-asset > should add image rules correctly 1`] = ` }, ], }, + "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", + }, "plugins": [ RsbuildCorePlugin {}, ], @@ -234,6 +237,9 @@ exports[`plugin-asset > should add image rules correctly 2`] = ` }, ], }, + "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", + }, "plugins": [ RsbuildCorePlugin {}, ], @@ -354,6 +360,9 @@ exports[`plugin-asset > should allow to use distPath.image to modify dist path 1 }, ], }, + "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", + }, "plugins": [ RsbuildCorePlugin {}, ], @@ -474,6 +483,9 @@ exports[`plugin-asset > should allow to use filename.image to modify filename 1` }, ], }, + "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", + }, "plugins": [ RsbuildCorePlugin {}, ], diff --git a/packages/core/tests/__snapshots__/builder.test.ts.snap b/packages/core/tests/__snapshots__/builder.test.ts.snap index c8f439e194..bbe37537ae 100644 --- a/packages/core/tests/__snapshots__/builder.test.ts.snap +++ b/packages/core/tests/__snapshots__/builder.test.ts.snap @@ -291,6 +291,7 @@ exports[`should use rspack as default bundler > apply rspack correctly 1`] = ` }, }, "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "static/js/async/[name].js", "devtoolModuleFilenameTemplate": [Function], "filename": "static/js/[name].js", diff --git a/packages/core/tests/__snapshots__/default.test.ts.snap b/packages/core/tests/__snapshots__/default.test.ts.snap index 0198b7185f..16bf4d9188 100644 --- a/packages/core/tests/__snapshots__/default.test.ts.snap +++ b/packages/core/tests/__snapshots__/default.test.ts.snap @@ -291,6 +291,7 @@ exports[`applyDefaultPlugins > should apply default plugins correctly 1`] = ` }, }, "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "static/js/async/[name].js", "devtoolModuleFilenameTemplate": [Function], "filename": "static/js/[name].js", @@ -718,6 +719,7 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when prod }, }, "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "static/js/async/[name].[contenthash:8].js", "filename": "static/js/[name].[contenthash:8].js", "hashFunction": "xxhash64", @@ -1103,6 +1105,7 @@ exports[`applyDefaultPlugins > should apply default plugins correctly when targe "splitChunks": false, }, "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "[name].js", "filename": "[name].js", "hashFunction": "xxhash64", @@ -1454,6 +1457,7 @@ exports[`tools.rspack > should match snapshot 1`] = ` }, }, "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "static/js/async/[name].js", "devtoolModuleFilenameTemplate": [Function], "filename": "static/js/[name].js", diff --git a/packages/core/tests/__snapshots__/environments.test.ts.snap b/packages/core/tests/__snapshots__/environments.test.ts.snap index 3811ad6bd7..50734250f7 100644 --- a/packages/core/tests/__snapshots__/environments.test.ts.snap +++ b/packages/core/tests/__snapshots__/environments.test.ts.snap @@ -49,6 +49,7 @@ exports[`environment config > should normalize environment config correctly 1`] "svg": 4096, }, "distPath": { + "assets": "static/assets", "css": "static/css", "font": "static/font", "html": "./", @@ -176,6 +177,7 @@ exports[`environment config > should normalize environment config correctly 2`] "svg": 4096, }, "distPath": { + "assets": "static/assets", "css": "static/css", "font": "static/font", "html": "./", @@ -303,6 +305,7 @@ exports[`environment config > should print environment config when inspect confi "svg": 4096, }, "distPath": { + "assets": "static/assets", "css": "static/css", "font": "static/font", "html": "./", @@ -461,6 +464,7 @@ exports[`environment config > should print environment config when inspect confi "svg": 4096, }, "distPath": { + "assets": "static/assets", "css": "static/css", "font": "static/font", "html": "./", @@ -637,6 +641,7 @@ exports[`environment config > should support modify environment config by api.mo "svg": 4096, }, "distPath": { + "assets": "static/assets", "css": "static/css", "font": "static/font", "html": "./", @@ -796,6 +801,7 @@ exports[`environment config > should support modify environment config by api.mo "svg": 4096, }, "distPath": { + "assets": "static/assets", "css": "static/css", "font": "static/font", "html": "./", @@ -956,6 +962,7 @@ exports[`environment config > should support modify environment config by api.mo "svg": 4096, }, "distPath": { + "assets": "static/assets", "css": "static/css", "font": "static/font", "html": "./", @@ -1119,6 +1126,7 @@ exports[`environment config > should support modify single environment config by "svg": 4096, }, "distPath": { + "assets": "static/assets", "css": "static/css", "font": "static/font", "html": "./", @@ -1278,6 +1286,7 @@ exports[`environment config > should support modify single environment config by "svg": 4096, }, "distPath": { + "assets": "static/assets", "css": "static/css", "font": "static/font", "html": "./", @@ -1681,6 +1690,7 @@ exports[`environment config > tools.rspack / bundlerChain can be configured in e }, }, "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "static/js/async/[name].js", "devtoolModuleFilenameTemplate": [Function], "filename": "static/js/[name].js", @@ -2008,6 +2018,7 @@ exports[`environment config > tools.rspack / bundlerChain can be configured in e "splitChunks": false, }, "output": { + "assetModuleFilename": "static/assets/[name].[contenthash:8][ext]", "chunkFilename": "[name].js", "devtoolModuleFilenameTemplate": [Function], "filename": "bundle.js", diff --git a/website/docs/en/config/output/dist-path.mdx b/website/docs/en/config/output/dist-path.mdx index 95997c4958..10802a5c18 100644 --- a/website/docs/en/config/output/dist-path.mdx +++ b/website/docs/en/config/output/dist-path.mdx @@ -15,6 +15,7 @@ type DistPathConfig = { wasm?: string; image?: string; media?: string; + assets?: string; }; ``` @@ -33,6 +34,7 @@ const defaultDistPath = { wasm: 'static/wasm', image: 'static/image', media: 'static/media', + assets: 'static/assets', }; ``` @@ -51,6 +53,7 @@ Detail: - `wasm`: The output directory of WebAssembly files. - `image`: The output directory of non-SVG images. - `media`: The output directory of media assets, such as videos. +- `assets`: The output directory of assets except the types mentioned above, which also match [Asset Modules](https://rspack.dev/guide/features/asset-module). ### Root Directory diff --git a/website/docs/en/config/output/filename.mdx b/website/docs/en/config/output/filename.mdx index bfb62bdc41..02d74ac87f 100644 --- a/website/docs/en/config/output/filename.mdx +++ b/website/docs/en/config/output/filename.mdx @@ -13,6 +13,7 @@ type FilenameConfig = { font?: string; image?: string; media?: string; + assets?: string; }; ``` @@ -28,6 +29,7 @@ const devDefaultFilename = { font: '[name].[contenthash:8][ext]', image: '[name].[contenthash:8][ext]', media: '[name].[contenthash:8][ext]', + assets: '[name].[contenthash:8][ext]', }; // Production mode @@ -39,6 +41,7 @@ const prodDefaultFilename = { font: '[name].[contenthash:8][ext]', image: '[name].[contenthash:8][ext]', media: '[name].[contenthash:8][ext]', + assets: '[name].[contenthash:8][ext]', }; ``` @@ -55,6 +58,7 @@ The following are the details of each filename: - `font`: The name of the font files. - `image`: The name of non-SVG images. - `media`: The name of media assets, such as video. +- `assets`: The name of assets except the types mentioned above, which match [Asset Modules](https://rspack.dev/guide/features/asset-module). > See [Output Files](/guide/basic/output-files) for more information. diff --git a/website/docs/en/guide/basic/static-assets.mdx b/website/docs/en/guide/basic/static-assets.mdx index 43b2074df7..05b0e28b9d 100644 --- a/website/docs/en/guide/basic/static-assets.mdx +++ b/website/docs/en/guide/basic/static-assets.mdx @@ -23,7 +23,7 @@ SVG image is a special case. Rsbuild support convert SVG to React components, so ## Import Assets in JS file -In JS files, you can import static assets in relative paths: +In JS files, you can import static assets with relative paths: ```tsx // Import the logo.png image in the static directory @@ -32,7 +32,7 @@ import logo from './static/logo.png'; export default = () => ; ``` -Import with [alias](/guide/advanced/alias) are also supported: +Import with [alias](/guide/advanced/alias) is also available: ```tsx import logo from '@/static/logo.png'; @@ -40,6 +40,14 @@ import logo from '@/static/logo.png'; export default = () => ; ``` +You can also combine [URL](https://developer.mozilla.org/docs/Web/API/URL) with [import.meta.url](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/import.meta) which are native ESM features to import static assets. + +```tsx +const logo = new URL('./static/logo.png', import.meta.url).href; + +export default = () => ; +``` + ## Import Assets in CSS file In CSS files, you can reference static assets in relative paths: diff --git a/website/docs/zh/config/output/dist-path.mdx b/website/docs/zh/config/output/dist-path.mdx index 14b0eaeea9..93c2ccb0f6 100644 --- a/website/docs/zh/config/output/dist-path.mdx +++ b/website/docs/zh/config/output/dist-path.mdx @@ -15,6 +15,7 @@ type DistPathConfig = { wasm?: string; image?: string; media?: string; + assets?: string; }; ``` @@ -33,6 +34,7 @@ const defaultDistPath = { wasm: 'static/wasm', image: 'static/image', media: 'static/media', + assets: 'static/assets'; }; ``` @@ -51,6 +53,7 @@ const defaultDistPath = { - `wasm`:表示 WebAssembly 文件的输出目录。 - `image`:表示非 SVG 图片的输出目录。 - `media`:表示视频等媒体资源的输出目录。 +- `assets`:表示除了上述类型之外,其他也命中 [Asset Modules](https://rspack.dev/guide/features/asset-module) 的文件的输出目录。 ### 根目录 diff --git a/website/docs/zh/config/output/filename.mdx b/website/docs/zh/config/output/filename.mdx index f8713be63a..07a860bc73 100644 --- a/website/docs/zh/config/output/filename.mdx +++ b/website/docs/zh/config/output/filename.mdx @@ -28,6 +28,7 @@ const devDefaultFilename = { font: '[name].[contenthash:8][ext]', image: '[name].[contenthash:8][ext]', media: '[name].[contenthash:8][ext]', + assets: '[name].[contenthash:8][ext]', }; // 生产模式构建 @@ -39,6 +40,7 @@ const prodDefaultFilename = { font: '[name].[contenthash:8][ext]', image: '[name].[contenthash:8][ext]', media: '[name].[contenthash:8][ext]', + assets: '[name].[contenthash:8][ext]', }; ``` @@ -55,6 +57,7 @@ const prodDefaultFilename = { - `font`:表示字体文件的名称。 - `image`:表示非 SVG 图片的名称。 - `media`:表示视频等媒体资源的名称。 +- `assets`:表示除了上述类型之外,其他命中 [Asset Modules](https://rspack.dev/guide/features/asset-module) 的文件名称。 > 查看 [构建产物目录](/guide/basic/output-files) 了解更多。 diff --git a/website/docs/zh/guide/basic/static-assets.mdx b/website/docs/zh/guide/basic/static-assets.mdx index eb746ccfb1..c14d3d5961 100644 --- a/website/docs/zh/guide/basic/static-assets.mdx +++ b/website/docs/zh/guide/basic/static-assets.mdx @@ -32,7 +32,7 @@ import logo from './static/logo.png'; export default = () => ; ``` -也支持使用[路径别名](/guide/advanced/alias)来引用: +也可以使用[路径别名](/guide/advanced/alias)来引用: ```tsx import logo from '@/static/logo.png'; @@ -40,6 +40,14 @@ import logo from '@/static/logo.png'; export default = () => ; ``` +也支持使用 ESM 原生功能中的 [URL](https://developer.mozilla.org/docs/Web/API/URL) 和 [import.meta.url](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/import.meta) 相配合,来引用静态资源。 + +```tsx +const logo = new URL('./static/logo.png', import.meta.url).href; + +export default = () => ; +``` + ## 在 CSS 文件中引用 在 CSS 文件中,可以引用相对路径下的静态资源: