diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index c82dff358..acea587fc 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -889,13 +889,14 @@ const composeEntryConfig = async ( bundle: LibConfig['bundle'], root: string, cssModulesAuto: CssLoaderOptionsAuto, -): Promise<{ entryConfig: EnvironmentConfig; lcp: string | null }> => { + userOutBase?: string, +): Promise<{ entryConfig: EnvironmentConfig; outBase: string | null }> => { let entries: RsbuildConfigEntry = rawEntry; if (!entries) { // In bundle mode, return directly to let Rsbuild apply default entry to './src/index.ts' if (bundle !== false) { - return { entryConfig: {}, lcp: null }; + return { entryConfig: {}, outBase: null }; } // In bundleless mode, set default entry to './src/**' @@ -957,13 +958,22 @@ const composeEntryConfig = async ( entry: appendEntryQuery(resolveEntryPath(entries, root)), }, }, - lcp: null, + outBase: null, }; } - const scanGlobEntries = async (calcLcp: boolean) => { + const scanGlobEntries = async (tryResolveOutBase: boolean) => { // In bundleless mode, resolve glob patterns and convert them to entry object. const resolvedEntries: Record = {}; + + const resolveOutBase = async (resolvedEntryFiles: string[]) => { + // Similar to `rootDir` in tsconfig and `outbase` in esbuild. + const lcp = await calcLongestCommonPath(resolvedEntryFiles); + // Using the longest common path of all non-declaration input files by default. + const outBase = userOutBase ?? (lcp === null ? root : lcp); + return outBase; + }; + for (const key of Object.keys(entries)) { const entry = entries[key]; @@ -998,10 +1008,7 @@ const composeEntryConfig = async ( throw new Error(`Cannot find ${resolvedEntryFiles}`); } - // Similar to `rootDir` in tsconfig and `outbase` in esbuild. - const lcp = await calcLongestCommonPath(resolvedEntryFiles); - // Using the longest common path of all non-declaration input files by default. - const outBase = lcp === null ? root : lcp; + const outBase = await resolveOutBase(resolvedEntryFiles); function getEntryName(file: string) { const { dir, name } = path.parse(path.relative(outBase, file)); @@ -1021,7 +1028,7 @@ const composeEntryConfig = async ( const entryName = getEntryName(file); if (resolvedEntries[entryName]) { - calcLcp && + tryResolveOutBase && logger.warn( `Duplicate entry ${color.cyan(entryName)} from ${color.cyan( path.relative(root, file), @@ -1035,15 +1042,15 @@ const composeEntryConfig = async ( } } - if (calcLcp) { - const lcp = await calcLongestCommonPath(Object.values(resolvedEntries)); - return { resolvedEntries, lcp }; + if (tryResolveOutBase) { + const outBase = await resolveOutBase(Object.values(resolvedEntries)); + return { resolvedEntries, outBase }; } - return { resolvedEntries, lcp: null }; + return { resolvedEntries, outBase: null }; }; - // LCP could only be determined at the first time of glob scan. - const { lcp } = await scanGlobEntries(true); + // OutBase could only be determined at the first time of glob scan. + const { outBase } = await scanGlobEntries(true); const entryConfig: EnvironmentConfig = { tools: { rspack: { @@ -1057,7 +1064,7 @@ const composeEntryConfig = async ( return { entryConfig, - lcp, + outBase, }; }; @@ -1415,14 +1422,15 @@ async function composeLibRsbuildConfig( pkgJson, userExternals: config.output?.externals, }); - const { entryConfig, lcp } = await composeEntryConfig( + const { entryConfig, outBase } = await composeEntryConfig( config.source?.entry!, config.bundle, rootPath, cssModulesAuto, + config.outBase, ); const cssConfig = composeCssConfig( - lcp, + outBase, config.bundle, banner?.css, footer?.css, @@ -1431,7 +1439,7 @@ async function composeLibRsbuildConfig( const entryChunkConfig = composeEntryChunkConfig({ enabledImportMetaUrlShim: enabledShims.cjs['import.meta.url'], - contextToWatch: lcp, + contextToWatch: outBase, }); const dtsConfig = await composeDtsConfig(config, dtsExtension); const externalsWarnConfig = composeExternalsWarnConfig( @@ -1548,6 +1556,7 @@ export async function composeCreateRsbuildConfig( dts: true, shims: true, umdName: true, + outBase: true, }), ), }; diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index f183458d8..cc6b75b7f 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -285,6 +285,12 @@ export interface LibConfig extends EnvironmentConfig { * @see {@link https://lib.rsbuild.dev/config/lib/umd-name} */ umdName?: string; + /** + * The base directory of the output files. + * @defaultValue `undefined` + * @see {@link https://lib.rsbuild.dev/config/lib/out-base} + */ + outBase?: string; } export type LibOnlyConfig = Omit; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6b5878e1..03dee98d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -750,6 +750,12 @@ importers: specifier: ^1.3.0 version: 1.3.0(@rsbuild/core@1.2.4) + tests/integration/outBase/assets-paths: {} + + tests/integration/outBase/custom-entry: {} + + tests/integration/outBase/nested-dir: {} + tests/integration/plugins/basic: {} tests/integration/plugins/mf-dev: {} diff --git a/tests/integration/outBase/custom-entry/package.json b/tests/integration/outBase/custom-entry/package.json new file mode 100644 index 000000000..00f837b28 --- /dev/null +++ b/tests/integration/outBase/custom-entry/package.json @@ -0,0 +1,6 @@ +{ + "name": "outbase-custom-entry-dir-test", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/tests/integration/outBase/custom-entry/rslib.config.ts b/tests/integration/outBase/custom-entry/rslib.config.ts new file mode 100644 index 000000000..6164415dd --- /dev/null +++ b/tests/integration/outBase/custom-entry/rslib.config.ts @@ -0,0 +1,34 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig({ + bundle: false, + source: { + entry: { + index: './src/utils/foo', + }, + }, + output: { + distPath: { + root: './dist/esm0', + }, + }, + }), + generateBundleEsmConfig({ + bundle: false, + outBase: './src/utils', + source: { + entry: { + index: './src/utils/foo', + }, + }, + output: { + distPath: { + root: './dist/esm1', + }, + }, + }), + ], +}); diff --git a/tests/integration/outBase/custom-entry/src/bar/index.ts b/tests/integration/outBase/custom-entry/src/bar/index.ts new file mode 100644 index 000000000..57a77b232 --- /dev/null +++ b/tests/integration/outBase/custom-entry/src/bar/index.ts @@ -0,0 +1 @@ +export const bar = 'foo'; diff --git a/tests/integration/outBase/custom-entry/src/utils/foo/index.ts b/tests/integration/outBase/custom-entry/src/utils/foo/index.ts new file mode 100644 index 000000000..3329a7d97 --- /dev/null +++ b/tests/integration/outBase/custom-entry/src/utils/foo/index.ts @@ -0,0 +1 @@ +export const foo = 'foo'; diff --git a/tests/integration/outBase/custom-entry/src/utils/index.ts b/tests/integration/outBase/custom-entry/src/utils/index.ts new file mode 100644 index 000000000..d2ef07f8a --- /dev/null +++ b/tests/integration/outBase/custom-entry/src/utils/index.ts @@ -0,0 +1,2 @@ +export { foo } from './foo'; +export { bar } from './bar'; diff --git a/tests/integration/outBase/index.test.ts b/tests/integration/outBase/index.test.ts new file mode 100644 index 000000000..c204872ac --- /dev/null +++ b/tests/integration/outBase/index.test.ts @@ -0,0 +1,47 @@ +import { join } from 'node:path'; +import { buildAndGetResults } from 'test-helper'; +import { describe, expect, test } from 'vitest'; + +describe('outBase', async () => { + test('base', async () => { + const fixturePath = join(__dirname, 'nested-dir'); + const { files } = await buildAndGetResults({ + fixturePath, + }); + + expect(files.esm0!.sort()).toMatchInlineSnapshot(` + [ + "/tests/integration/outBase/nested-dir/dist/esm0/bar/index.js", + "/tests/integration/outBase/nested-dir/dist/esm0/foo/index.js", + "/tests/integration/outBase/nested-dir/dist/esm0/index.js", + ] + `); + + expect(files.esm1!.sort()).toMatchInlineSnapshot(` + [ + "/tests/integration/outBase/nested-dir/dist/esm1/utils/bar/index.js", + "/tests/integration/outBase/nested-dir/dist/esm1/utils/foo/index.js", + "/tests/integration/outBase/nested-dir/dist/esm1/utils/index.js", + ] + `); + }); + + test('with custom entry', async () => { + const fixturePath = join(__dirname, 'custom-entry'); + const { files } = await buildAndGetResults({ + fixturePath, + }); + + expect(files.esm0!.sort()).toMatchInlineSnapshot(` + [ + "/tests/integration/outBase/custom-entry/dist/esm0/index.js", + ] + `); + + expect(files.esm1!.sort()).toMatchInlineSnapshot(` + [ + "/tests/integration/outBase/custom-entry/dist/esm1/foo/index.js", + ] + `); + }); +}); diff --git a/tests/integration/outBase/nested-dir/package.json b/tests/integration/outBase/nested-dir/package.json new file mode 100644 index 000000000..e6b25b068 --- /dev/null +++ b/tests/integration/outBase/nested-dir/package.json @@ -0,0 +1,6 @@ +{ + "name": "outbase-nested-dir-test", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/tests/integration/outBase/nested-dir/rslib.config.ts b/tests/integration/outBase/nested-dir/rslib.config.ts new file mode 100644 index 000000000..a29ec498b --- /dev/null +++ b/tests/integration/outBase/nested-dir/rslib.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig({ + bundle: false, + dts: true, + output: { + distPath: { + root: './dist/esm0', + }, + }, + }), + generateBundleEsmConfig({ + bundle: false, + dts: true, + outBase: './src', + output: { + distPath: { + root: './dist/esm1', + }, + }, + }), + ], +}); diff --git a/tests/integration/outBase/nested-dir/src/utils/bar/index.ts b/tests/integration/outBase/nested-dir/src/utils/bar/index.ts new file mode 100644 index 000000000..57a77b232 --- /dev/null +++ b/tests/integration/outBase/nested-dir/src/utils/bar/index.ts @@ -0,0 +1 @@ +export const bar = 'foo'; diff --git a/tests/integration/outBase/nested-dir/src/utils/foo/index.ts b/tests/integration/outBase/nested-dir/src/utils/foo/index.ts new file mode 100644 index 000000000..3329a7d97 --- /dev/null +++ b/tests/integration/outBase/nested-dir/src/utils/foo/index.ts @@ -0,0 +1 @@ +export const foo = 'foo'; diff --git a/tests/integration/outBase/nested-dir/src/utils/index.ts b/tests/integration/outBase/nested-dir/src/utils/index.ts new file mode 100644 index 000000000..d2ef07f8a --- /dev/null +++ b/tests/integration/outBase/nested-dir/src/utils/index.ts @@ -0,0 +1,2 @@ +export { foo } from './foo'; +export { bar } from './bar';