diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 71dffc357..fd8dde6dc 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -973,10 +973,11 @@ const composeExternalsConfig = ( }; }; -const composeAutoExtensionConfig = ( +const composeOutputFilenameConfig = ( config: LibConfig, format: Format, autoExtension: boolean, + multiCompilerIndex: number | null, pkgJson?: PkgJson, ): { config: EnvironmentConfig; @@ -998,9 +999,38 @@ const composeAutoExtensionConfig = ( return filenameHash ? '.[contenthash:8]' : ''; }; + // Copied from https://github.com/web-infra-dev/rspack/blob/2efea8673f86a562559e26a9351680e8df4d9ae9/packages/rspack/src/config/defaults.ts#L667-L680. + const inferChunkFilename = (filename: string): string | undefined => { + if (typeof filename !== 'function') { + const hasName = filename.includes('[name]'); + const hasId = filename.includes('[id]'); + const hasChunkHash = filename.includes('[chunkhash]'); + const hasContentHash = filename.includes('[contenthash]'); + const multiCompilerPrefix = + typeof multiCompilerIndex === 'number' ? `${multiCompilerIndex}~` : ''; + // Anything changing depending on chunk is fine + + if (hasChunkHash || hasContentHash || hasName || hasId) + return filename.replace( + /(^|\/)([^/]*(?:\?|$))/, + `$1${multiCompilerPrefix}$2`, + ); + // Otherwise prefix "[id]." in front of the basename to make it changing + return filename.replace( + /(^|\/)([^/]*(?:\?|$))/, + `$1${multiCompilerIndex}[id].$2`, + ); + } + + return undefined; + }; + const hash = getHash(); const defaultJsFilename = `[name]${hash}${jsExtension}`; const userJsFilename = config.output?.filename?.js; + const defaultJsChunkFilename = inferChunkFilename( + (userJsFilename as string) ?? defaultJsFilename, + ); // will be returned to use in redirect feature // only support string type for now since we can not get the return value of function @@ -1009,15 +1039,25 @@ const composeAutoExtensionConfig = ( ? extname(userJsFilename) : jsExtension; - const finalConfig = userJsFilename - ? {} - : { + const chunkFilename: RsbuildConfig = { + tools: { + rspack: { + output: { + chunkFilename: defaultJsChunkFilename, + }, + }, + }, + }; + + const finalConfig: RsbuildConfig = userJsFilename + ? chunkFilename + : mergeRsbuildConfig(chunkFilename, { output: { filename: { js: defaultJsFilename, }, }, - }; + }); return { config: finalConfig, @@ -1604,6 +1644,7 @@ const composeExternalHelpersConfig = ( async function composeLibRsbuildConfig( config: LibConfig, + multiCompilerIndex: number | null, // null means there's non multi-compiler root?: string, sharedPlugins?: RsbuildPlugins, ) { @@ -1649,10 +1690,16 @@ async function composeLibRsbuildConfig( config.output?.externals, ); const { - config: autoExtensionConfig, + config: outputFilenameConfig, jsExtension, dtsExtension, - } = composeAutoExtensionConfig(config, format, autoExtension, pkgJson); + } = composeOutputFilenameConfig( + config, + format, + autoExtension, + multiCompilerIndex, + pkgJson, + ); const { entryConfig, outBase } = await composeEntryConfig( config.source?.entry!, config.bundle, @@ -1660,6 +1707,7 @@ async function composeLibRsbuildConfig( cssModulesAuto, config.outBase, ); + const { config: bundlelessExternalConfig } = composeBundlelessExternalConfig( jsExtension, redirect, @@ -1709,10 +1757,11 @@ async function composeLibRsbuildConfig( return mergeRsbuildConfig( formatConfig, + // outputConfig, shimsConfig, syntaxConfig, externalHelpersConfig, - autoExtensionConfig, + outputFilenameConfig, targetConfig, // #region Externals configs // The order of the externals config should come in the following order: @@ -1761,7 +1810,7 @@ export async function composeCreateRsbuildConfig( ); } - const libConfigPromises = libConfigsArray.map(async (libConfig) => { + const libConfigPromises = libConfigsArray.map(async (libConfig, index) => { const userConfig = mergeRsbuildConfig( sharedRsbuildConfig, libConfig, @@ -1771,6 +1820,7 @@ export async function composeCreateRsbuildConfig( // configuration and Lib configuration in the settings. const libRsbuildConfig = await composeLibRsbuildConfig( userConfig, + libConfigsArray.length > 1 ? index : null, root, sharedPlugins, ); diff --git a/packages/core/tests/__snapshots__/config.test.ts.snap b/packages/core/tests/__snapshots__/config.test.ts.snap index 0290c6744..090130684 100644 --- a/packages/core/tests/__snapshots__/config.test.ts.snap +++ b/packages/core/tests/__snapshots__/config.test.ts.snap @@ -393,7 +393,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i output: { path: '/dist', filename: '[name].js', - chunkFilename: '[name].js', + chunkFilename: '0~[name].js', publicPath: 'auto', pathinfo: false, hashFunction: 'xxhash64', @@ -1091,7 +1091,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i output: { path: '/dist', filename: '[name].cjs', - chunkFilename: '[name].cjs', + chunkFilename: '1~[name].cjs', publicPath: 'auto', pathinfo: false, hashFunction: 'xxhash64', @@ -1775,7 +1775,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i output: { path: '/dist', filename: '[name].js', - chunkFilename: '[name].js', + chunkFilename: '2~[name].js', publicPath: '/', pathinfo: false, hashFunction: 'xxhash64', @@ -2375,7 +2375,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i output: { path: '/dist', filename: '[name].js', - chunkFilename: '[name].js', + chunkFilename: '3~[name].js', publicPath: '/', pathinfo: false, hashFunction: 'xxhash64', @@ -2918,7 +2918,7 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i output: { path: '/dist', filename: '[name].js', - chunkFilename: '[name].js', + chunkFilename: '4~[name].js', publicPath: '/', pathinfo: false, hashFunction: 'xxhash64', @@ -3695,6 +3695,11 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i ], }, [Function], + { + "output": { + "chunkFilename": "0~[name].js", + }, + }, { "target": [ "node", @@ -3966,6 +3971,11 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i ], }, [Function], + { + "output": { + "chunkFilename": "1~[name].cjs", + }, + }, { "target": [ "node", @@ -4199,6 +4209,11 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i ], }, [Function], + { + "output": { + "chunkFilename": "2~[name].js", + }, + }, { "target": [ "node", @@ -4431,6 +4446,11 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i ], }, [Function], + { + "output": { + "chunkFilename": "3~[name].js", + }, + }, { "target": [ "node", @@ -4590,6 +4610,11 @@ exports[`Should compose create Rsbuild config correctly > Merge Rsbuild config i }, [Function], [Function], + { + "output": { + "chunkFilename": "4~[name].js", + }, + }, { "target": [ "web", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8dd00d747..a0a040640 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -966,6 +966,10 @@ importers: tests/integration/outBase/nested-dir: {} + tests/integration/output/chunkFileName-multi: {} + + tests/integration/output/chunkFileName-single: {} + tests/integration/plugins/basic: {} tests/integration/plugins/mf-dev: {} @@ -7657,8 +7661,8 @@ packages: vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - vue-component-type-helpers@3.0.6: - resolution: {integrity: sha512-6CRM8X7EJqWCJOiKPvSLQG+hJPb/Oy2gyJx3pLjUEhY7PuaCthQu3e0zAGI1lqUBobrrk9IT0K8sG2GsCluxoQ==} + vue-component-type-helpers@3.0.7: + resolution: {integrity: sha512-TvyUcFXmjZcXUvU+r1MOyn4/vv4iF+tPwg5Ig33l/FJ3myZkxeQpzzQMLMFWcQAjr6Xs7BRwVy/TwbmNZUA/4w==} vue-docgen-loader@1.5.1: resolution: {integrity: sha512-coMmQYsg+fy18SVtBNU7/tztdqEyrneFfwQFLmx8O7jaJ11VZ//9tRWXlwGzJM07cPRwMHDKMlAdWrpuw3U46A==} @@ -9958,7 +9962,7 @@ snapshots: storybook: 9.1.5(@testing-library/dom@10.4.0)(prettier@3.6.2)(vite@6.3.5(@types/node@22.18.1)(jiti@2.5.1)(sass-embedded@1.90.0)(sass@1.90.0)(stylus@0.64.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.6.1)) type-fest: 2.19.0 vue: 3.5.21(typescript@5.9.2) - vue-component-type-helpers: 3.0.6 + vue-component-type-helpers: 3.0.7 '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.0)': dependencies: @@ -15259,7 +15263,7 @@ snapshots: vscode-uri@3.0.8: {} - vue-component-type-helpers@3.0.6: {} + vue-component-type-helpers@3.0.7: {} vue-docgen-loader@1.5.1: dependencies: diff --git a/tests/integration/output/chunkFileName-multi/package.json b/tests/integration/output/chunkFileName-multi/package.json new file mode 100644 index 000000000..6c9775296 --- /dev/null +++ b/tests/integration/output/chunkFileName-multi/package.json @@ -0,0 +1,6 @@ +{ + "name": "minify-output-chunk-file-name-multi-test", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/tests/integration/output/chunkFileName-multi/rslib1.config.ts b/tests/integration/output/chunkFileName-multi/rslib1.config.ts new file mode 100644 index 000000000..cef6436d9 --- /dev/null +++ b/tests/integration/output/chunkFileName-multi/rslib1.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig({ + source: { + entry: { + lib1: './src/lib1.js', + }, + }, + }), + generateBundleEsmConfig({ + source: { + entry: { + lib2: './src/lib2.js', + }, + }, + }), + ], +}); diff --git a/tests/integration/output/chunkFileName-multi/rslib2.config.ts b/tests/integration/output/chunkFileName-multi/rslib2.config.ts new file mode 100644 index 000000000..cc47da2e8 --- /dev/null +++ b/tests/integration/output/chunkFileName-multi/rslib2.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + output: { + filename: { + js: 'static/js/[name].[contenthash:8].js', + }, + }, + lib: [ + generateBundleEsmConfig({ + source: { + entry: { + lib1: './src/lib1.js', + }, + }, + }), + generateBundleEsmConfig({ + source: { + entry: { + lib2: './src/lib2.js', + }, + }, + }), + ], +}); diff --git a/tests/integration/output/chunkFileName-multi/rslib3.config.ts b/tests/integration/output/chunkFileName-multi/rslib3.config.ts new file mode 100644 index 000000000..89f722e8c --- /dev/null +++ b/tests/integration/output/chunkFileName-multi/rslib3.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig({ + source: { + entry: { + lib1: './src/lib1.js', + }, + }, + output: { + filename: { + js: 'static1/js/[name].js', + }, + }, + }), + generateBundleEsmConfig({ + source: { + entry: { + lib2: './src/lib2.js', + }, + }, + output: { + filename: { + js: 'static2/js/[name].[contenthash:8].js', + }, + }, + }), + ], +}); diff --git a/tests/integration/output/chunkFileName-multi/src/lib1.js b/tests/integration/output/chunkFileName-multi/src/lib1.js new file mode 100644 index 000000000..74d4167f5 --- /dev/null +++ b/tests/integration/output/chunkFileName-multi/src/lib1.js @@ -0,0 +1,4 @@ +export default async function main() { + const { foo } = await import('./shared.js'); + return foo; +} diff --git a/tests/integration/output/chunkFileName-multi/src/lib2.js b/tests/integration/output/chunkFileName-multi/src/lib2.js new file mode 100644 index 000000000..923eeb2bb --- /dev/null +++ b/tests/integration/output/chunkFileName-multi/src/lib2.js @@ -0,0 +1,4 @@ +export default async function main() { + const { bar } = await import('./shared.js'); + return bar; +} diff --git a/tests/integration/output/chunkFileName-multi/src/shared.js b/tests/integration/output/chunkFileName-multi/src/shared.js new file mode 100644 index 000000000..04f7f43eb --- /dev/null +++ b/tests/integration/output/chunkFileName-multi/src/shared.js @@ -0,0 +1,2 @@ +export const foo = 'foo'; +export const bar = 'bar'; diff --git a/tests/integration/output/chunkFileName-single/package.json b/tests/integration/output/chunkFileName-single/package.json new file mode 100644 index 000000000..a8781a247 --- /dev/null +++ b/tests/integration/output/chunkFileName-single/package.json @@ -0,0 +1,6 @@ +{ + "name": "minify-output-chunk-file-name-single-test", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/tests/integration/output/chunkFileName-single/rslib.config.ts b/tests/integration/output/chunkFileName-single/rslib.config.ts new file mode 100644 index 000000000..882441054 --- /dev/null +++ b/tests/integration/output/chunkFileName-single/rslib.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig({ + source: { + entry: { + lib1: './src/lib1.js', + }, + }, + }), + ], +}); diff --git a/tests/integration/output/chunkFileName-single/src/lib1.js b/tests/integration/output/chunkFileName-single/src/lib1.js new file mode 100644 index 000000000..74d4167f5 --- /dev/null +++ b/tests/integration/output/chunkFileName-single/src/lib1.js @@ -0,0 +1,4 @@ +export default async function main() { + const { foo } = await import('./shared.js'); + return foo; +} diff --git a/tests/integration/output/chunkFileName-single/src/shared.js b/tests/integration/output/chunkFileName-single/src/shared.js new file mode 100644 index 000000000..04f7f43eb --- /dev/null +++ b/tests/integration/output/chunkFileName-single/src/shared.js @@ -0,0 +1,2 @@ +export const foo = 'foo'; +export const bar = 'bar'; diff --git a/tests/integration/output/index.test.ts b/tests/integration/output/index.test.ts new file mode 100644 index 000000000..ba266e9e4 --- /dev/null +++ b/tests/integration/output/index.test.ts @@ -0,0 +1,93 @@ +import { basename, join } from 'node:path'; +import { describe, expect, test } from '@rstest/core'; +import { buildAndGetResults } from 'test-helper'; + +describe('output config', () => { + describe('chunkFileName', () => { + test('should prefix index for multi-compiler builds', async () => { + const fixturePath = join(__dirname, 'chunkFileName-multi'); + const { files, rspackConfig } = await buildAndGetResults({ + fixturePath, + configPath: 'rslib1.config.ts', + }); + + expect(rspackConfig.length).toBeGreaterThanOrEqual(2); + expect(rspackConfig[0]!.output?.chunkFilename).toBe('0~[name].js'); + expect(rspackConfig[1]!.output?.chunkFilename).toBe('1~[name].js'); + + const esm0BaseNames = (files.esm0 ?? []).map((p) => basename(p)); + const esm1BaseNames = (files.esm1 ?? []).map((p) => basename(p)); + + expect(esm0BaseNames.some((n) => /^0~188\.js$/.test(n))).toBeTruthy(); + expect(esm1BaseNames.some((n) => /^1~188\.js$/.test(n))).toBeTruthy(); + }); + + test('should prefix index for multi-compiler builds (with filename)', async () => { + const fixturePath = join(__dirname, 'chunkFileName-multi'); + const { files, rspackConfig } = await buildAndGetResults({ + fixturePath, + configPath: 'rslib2.config.ts', + }); + + expect(rspackConfig.length).toBeGreaterThanOrEqual(2); + expect(rspackConfig[0]!.output?.chunkFilename).toBe( + 'static/js/0~[name].[contenthash:8].js', + ); + expect(rspackConfig[1]!.output?.chunkFilename).toBe( + 'static/js/1~[name].[contenthash:8].js', + ); + + const esm0BaseNames = (files.esm0 ?? []).map((p) => basename(p)); + const esm1BaseNames = (files.esm1 ?? []).map((p) => basename(p)); + + expect( + esm0BaseNames.some((n) => /^0~188\.\w+\.js$/.test(n)), + ).toBeTruthy(); + expect( + esm1BaseNames.some((n) => /^1~188\.\w+\.js$/.test(n)), + ).toBeTruthy(); + }); + + test('should prefix index for multi-compiler builds (with chunkFilename)', async () => { + const fixturePath = join(__dirname, 'chunkFileName-multi'); + const { files, rspackConfig } = await buildAndGetResults({ + fixturePath, + configPath: 'rslib3.config.ts', + }); + + expect(rspackConfig.length).toBeGreaterThanOrEqual(2); + expect(rspackConfig[0]!.output?.chunkFilename).toBe( + 'static1/js/0~[name].js', + ); + expect(rspackConfig[1]!.output?.chunkFilename).toBe( + 'static2/js/1~[name].[contenthash:8].js', + ); + + expect( + files.esm0!.some((n) => /static1\/js\/0~188\.js$/.test(n)), + ).toBeTruthy(); + expect( + files.esm0!.some((n) => /static1\/js\/lib1\.js$/.test(n)), + ).toBeTruthy(); + expect( + files.esm0!.some((n) => /static2\/js\/1~188\.\w+\.js$/.test(n)), + ).toBeTruthy(); + expect( + files.esm0!.some((n) => /static2\/js\/lib2\.\w+\.js$/.test(n)), + ).toBeTruthy(); + }); + + test('should not prefix index for single-compiler builds', async () => { + const fixturePath = join(__dirname, 'chunkFileName-single'); + const { files } = await buildAndGetResults({ fixturePath }); + + const esmBaseNames = (files.esm ?? []).map((p) => basename(p)); + expect(esmBaseNames.some((n) => /^\d+~.+\.js$/.test(n))).toBeFalsy(); + expect( + esmBaseNames.some( + (n) => /^\d+\.js$/.test(n) || /shared.*\.js$/.test(n), + ), + ).toBeTruthy(); + }); + }); +});