diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index 6230e0d64..60549bc50 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -54,7 +54,16 @@ export type Dts = * @defaultValue `false` * @see {@link https://rslib.rs/config/lib/dts#dtsbundle} */ - bundle?: boolean; + bundle?: + | boolean + | { + /** + * Specifies the dependencies whose declaration files should be bundled. + * @defaultValue {@link https://rslib.rs/config/lib/dts#dtsbundlebundledpackages} + * @see {@link https://rslib.rs/config/lib/dts#dtsbundlebundledpackages} + */ + bundledPackages?: string[]; + }; /** * The output directory of declaration files. * @defaultValue {@link https://rslib.rs/config/lib/dts#default-value} diff --git a/packages/plugin-dts/README.md b/packages/plugin-dts/README.md index d85200dd4..50daa7116 100644 --- a/packages/plugin-dts/README.md +++ b/packages/plugin-dts/README.md @@ -54,6 +54,33 @@ pluginDts({ }); ``` +#### bundle.bundledPackages + +- **Type:** `string[]` + +Specifies the dependencies whose declaration files should be bundled. This configuration is passed to the [bundledPackages](https://api-extractor.com/pages/configs/api-extractor_json/#bundledpackages) option of `@microsoft/api-extractor`. + +By default, `rsbuild-plugin-dts` determines externalized dependencies based on the following configurations. + +- [autoExternal](#autoexternal) configuration +- [output.externals](https://rsbuild.rs/config/output/externals) configuration + +Direct dependencies (declared in `package.json`) that are not externalized will be automatically added to `bundledPackages`, and their declaration files will be bundled into the final output. + +When the default behavior does not meet the requirements, you can explicitly specify the dependencies whose declaration files need to be bundled through `bundle.bundledPackages`. After setting this configuration, the above default behavior will be completely overwritten. + +This is typically used for bundling transitive dependencies (dependencies of direct dependencies). For example, if the project directly depends on `foo`, and `foo` depends on `bar`, you can bundle both `foo` and `bar`'s declaration files as follows: + +```js +pluginDts({ + bundle: { + bundledPackages: ['foo', 'bar'], + }, +}); +``` + +> `bundledPackages` can be specified with the [minimatch](https://www.npmjs.com/package/minimatch) syntax, but will only match the declared direct dependencies in `package.json`. + ### distPath - **Type:** `string` diff --git a/packages/plugin-dts/src/dts.ts b/packages/plugin-dts/src/dts.ts index 41818d836..edd4221a7 100644 --- a/packages/plugin-dts/src/dts.ts +++ b/packages/plugin-dts/src/dts.ts @@ -19,11 +19,16 @@ const isObject = (obj: unknown): obj is Record => // use !externals export const calcBundledPackages = (options: { - autoExternal: DtsGenOptions['autoExternal']; cwd: string; + autoExternal: DtsGenOptions['autoExternal']; userExternals?: DtsGenOptions['userExternals']; + overrideBundledPackages?: string[]; }): string[] => { - const { autoExternal, cwd, userExternals } = options; + const { cwd, autoExternal, userExternals, overrideBundledPackages } = options; + + if (overrideBundledPackages) { + return overrideBundledPackages; + } let pkgJson: { dependencies?: Record; @@ -119,6 +124,7 @@ export async function generateDts(data: DtsGenOptions): Promise { dtsExtension = '.d.ts', autoExternal = true, userExternals, + apiExtractorOptions, banner, footer, redirect = { @@ -213,9 +219,10 @@ export async function generateDts(data: DtsGenOptions): Promise { banner, footer, bundledPackages: calcBundledPackages({ - autoExternal, cwd, + autoExternal, userExternals, + overrideBundledPackages: apiExtractorOptions?.bundledPackages, }), }); } diff --git a/packages/plugin-dts/src/index.ts b/packages/plugin-dts/src/index.ts index ae7e96573..3fd5d6be0 100644 --- a/packages/plugin-dts/src/index.ts +++ b/packages/plugin-dts/src/index.ts @@ -22,8 +22,12 @@ export type DtsRedirect = { extension?: boolean; }; +export type ApiExtractorOptions = { + bundledPackages?: string[]; +}; + export type PluginDtsOptions = { - bundle?: boolean; + bundle?: boolean | ApiExtractorOptions; distPath?: string; build?: boolean; abortOnError?: boolean; @@ -46,7 +50,8 @@ export type DtsEntry = { path?: string; }; -export type DtsGenOptions = PluginDtsOptions & { +export type DtsGenOptions = Omit & { + bundle: boolean; name: string; cwd: string; isWatch: boolean; @@ -56,6 +61,7 @@ export type DtsGenOptions = PluginDtsOptions & { tsconfigPath: string; tsConfigResult: ts.ParsedCommandLine; userExternals?: NonNullable['externals']; + apiExtractorOptions?: ApiExtractorOptions; }; interface TaskResult { @@ -71,7 +77,15 @@ export const pluginDts = (options: PluginDtsOptions = {}): RsbuildPlugin => ({ name: PLUGIN_DTS_NAME, setup(api) { - options.bundle = options.bundle ?? false; + let apiExtractorOptions = {}; + + if (options.bundle && typeof options.bundle === 'object') { + apiExtractorOptions = { + ...options.bundle, + }; + } + + const bundle = !!options.bundle; options.abortOnError = options.abortOnError ?? true; options.build = options.build ?? false; options.redirect = options.redirect ?? {}; @@ -93,10 +107,7 @@ export const pluginDts = (options: PluginDtsOptions = {}): RsbuildPlugin => ({ // @microsoft/api-extractor only support single entry to bundle declaration files // see https://github.com/microsoft/rushstack/issues/1596#issuecomment-546790721 // we support multiple entries by calling api-extractor multiple times - const dtsEntry = processSourceEntry( - options.bundle!, - config.source?.entry, - ); + const dtsEntry = processSourceEntry(bundle, config.source?.entry); const cwd = api.context.rootPath; const tsconfigPath = ts.findConfigFile( @@ -132,7 +143,7 @@ export const pluginDts = (options: PluginDtsOptions = {}): RsbuildPlugin => ({ } // clean .rslib temp folder - if (options.bundle) { + if (bundle) { await clearTempDeclarationDir(cwd); } @@ -150,9 +161,11 @@ export const pluginDts = (options: PluginDtsOptions = {}): RsbuildPlugin => ({ const dtsGenOptions: DtsGenOptions = { ...options, + bundle, dtsEntry, dtsEmitPath, userExternals: config.output.externals, + apiExtractorOptions, tsconfigPath, tsConfigResult, name: environment.name, diff --git a/packages/plugin-dts/tests/external.test.ts b/packages/plugin-dts/tests/external.test.ts index 6578a3c30..2698d90b6 100644 --- a/packages/plugin-dts/tests/external.test.ts +++ b/packages/plugin-dts/tests/external.test.ts @@ -105,4 +105,18 @@ describe('should calcBundledPackages correctly', () => { expect(result).toEqual([]); }); + + it('overrides with bundledPackages', () => { + vi.spyOn(fs, 'readFileSync').mockImplementation(() => + JSON.stringify(commonPkgJson), + ); + + const result = calcBundledPackages({ + autoExternal: true, + cwd: 'pkg/to/root', + overrideBundledPackages: ['foo', '@bar/*'], + }); + + expect(result).toEqual(['foo', '@bar/*']); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d538bfe0..cd761752f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -749,6 +749,12 @@ importers: tests/integration/dts/bundle/bundle-name: {} + tests/integration/dts/bundle/bundled-packages: + devDependencies: + '@vitest/expect': + specifier: 3.1.4 + version: 3.1.4 + tests/integration/dts/bundle/clean: {} tests/integration/dts/bundle/dist-path: {} diff --git a/tests/integration/dts/bundle/bundled-packages/package.json b/tests/integration/dts/bundle/bundled-packages/package.json new file mode 100644 index 000000000..f0923c790 --- /dev/null +++ b/tests/integration/dts/bundle/bundled-packages/package.json @@ -0,0 +1,9 @@ +{ + "name": "dts-bundle-bundled-packages-test", + "version": "1.0.0", + "private": true, + "type": "module", + "devDependencies": { + "@vitest/expect": "3.1.4" + } +} diff --git a/tests/integration/dts/bundle/bundled-packages/rslib.config.ts b/tests/integration/dts/bundle/bundled-packages/rslib.config.ts new file mode 100644 index 000000000..4b5ad9d12 --- /dev/null +++ b/tests/integration/dts/bundle/bundled-packages/rslib.config.ts @@ -0,0 +1,41 @@ +import { defineConfig } from '@rslib/core'; +import { generateBundleEsmConfig } from 'test-helper'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig({ + dts: { + bundle: true, + }, + output: { + distPath: { + root: './dist/esm/default', + }, + }, + }), + generateBundleEsmConfig({ + dts: { + bundle: { + bundledPackages: [], + }, + }, + output: { + distPath: { + root: './dist/esm/override-empty-array', + }, + }, + }), + generateBundleEsmConfig({ + dts: { + bundle: { + bundledPackages: ['@vitest/expect', '@vitest/utils'], + }, + }, + output: { + distPath: { + root: './dist/esm/override-array-string', + }, + }, + }), + ], +}); diff --git a/tests/integration/dts/bundle/bundled-packages/src/index.ts b/tests/integration/dts/bundle/bundled-packages/src/index.ts new file mode 100644 index 000000000..ec47f5be9 --- /dev/null +++ b/tests/integration/dts/bundle/bundled-packages/src/index.ts @@ -0,0 +1 @@ +export * from '@vitest/expect'; diff --git a/tests/integration/dts/bundle/bundled-packages/tsconfig.json b/tests/integration/dts/bundle/bundled-packages/tsconfig.json new file mode 100644 index 000000000..888d3e460 --- /dev/null +++ b/tests/integration/dts/bundle/bundled-packages/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@rslib/tsconfig/base", + "compilerOptions": { + "baseUrl": "./" + }, + "include": ["src"] +} diff --git a/tests/integration/dts/index.test.ts b/tests/integration/dts/index.test.ts index 815dd2a90..89d62d30c 100644 --- a/tests/integration/dts/index.test.ts +++ b/tests/integration/dts/index.test.ts @@ -400,6 +400,33 @@ describe('dts when bundle: true', () => { restore(); }); + + test('override with bundledPackages', async () => { + const fixturePath = join(__dirname, 'bundle', 'bundled-packages'); + const { entries } = await buildAndGetResults({ + fixturePath, + type: 'dts', + }); + + // default + expect(entries.esm0).toContain( + `import { Constructable } from '@vitest/utils';`, + ); + + // override empty array + expect(entries.esm1).toMatchInlineSnapshot(` + " + export * from "@vitest/expect"; + + export { } + " + `); + + // override with bundledPackages + expect(entries.esm2).not.toContain( + `import { Constructable } from '@vitest/utils';`, + ); + }); }); describe('dts when build: true', () => { diff --git a/website/docs/en/config/lib/dts.mdx b/website/docs/en/config/lib/dts.mdx index c483c6cfe..7afe3bbb5 100644 --- a/website/docs/en/config/lib/dts.mdx +++ b/website/docs/en/config/lib/dts.mdx @@ -9,7 +9,7 @@ overviewHeaders: [2, 3] ```ts type Dts = | { - bundle?: boolean; + bundle?: boolean | { bundledPackages?: string[] }; distPath?: string; build?: boolean; abortOnError?: boolean; @@ -56,13 +56,11 @@ If you want to customize the declaration files generation, you can set the `dts` ### dts.bundle -- **Type:** `boolean` +- **Type:** `boolean | { bundledPackages?: string[] }` - **Default:** `false` Whether to bundle the declaration files. -#### Example - If you want to [bundle declaration files](/guide/advanced/dts#bundle-declaration-files) files, you should: 1. Install [@microsoft/api-extractor](https://www.npmjs.com/package/@microsoft/api-extractor) as a development dependency, which is the underlying tool used for bundling declaration files. @@ -87,9 +85,42 @@ export default { }; ``` -#### Handle third-party packages +#### dts.bundle.bundledPackages + +- **Type:** `string[]` + +Specifies the dependencies whose declaration files should be bundled. This configuration is passed to the [bundledPackages](https://api-extractor.com/pages/configs/api-extractor_json/#bundledpackages) option of `@microsoft/api-extractor`. + +By default, Rslib determines externalized dependencies based on the following configurations. For details, refer to [Handle third-party dependencies](/guide/advanced/third-party-deps). + +- [autoExternal](/config/lib/auto-external) configuration +- [output.externals](/config/rsbuild/output#outputexternals) configuration + +Direct dependencies (declared in `package.json`) that are not externalized will be automatically added to `bundledPackages`, and their declaration files will be bundled into the final output. + +When the default behavior does not meet the requirements, you can explicitly specify the dependencies whose declaration files need to be bundled through `dts.bundle.bundledPackages`. After setting this configuration, the above default behavior will be completely overwritten. + +This is typically used for bundling transitive dependencies (dependencies of direct dependencies). For example, if the project directly depends on `foo`, and `foo` depends on `bar`, you can bundle both `foo` and `bar`'s declaration files as follows: + +```ts title="rslib.config.ts" +export default { + lib: [ + { + format: 'esm', + dts: { + // [!code highlight:3] + bundle: { + bundledPackages: ['foo', 'bar'], + }, + }, + }, + ], +}; +``` -When we bundle declaration files, we should specify which third-party package types need to be bundled, refer to the [Handle Third-Party Dependencies](/guide/advanced/third-party-deps) documentation for more details about externals related configurations. +::: note +`bundledPackages` can be specified with the [minimatch](https://www.npmjs.com/package/minimatch) syntax, but will only match the declared direct dependencies in `package.json`. +::: ### dts.distPath diff --git a/website/docs/zh/config/lib/dts.mdx b/website/docs/zh/config/lib/dts.mdx index 9e5b0cafe..897807e9b 100644 --- a/website/docs/zh/config/lib/dts.mdx +++ b/website/docs/zh/config/lib/dts.mdx @@ -9,7 +9,7 @@ overviewHeaders: [2, 3] ```ts type Dts = | { - bundle?: boolean; + bundle?: boolean | { bundledPackages?: string[] }; distPath?: string; build?: boolean; abortOnError?: boolean; @@ -56,13 +56,11 @@ export default { ### dts.bundle -- **类型:** `boolean` +- **类型:** `boolean | { bundledPackages?: string[] }` - **默认值:** `false` 是否打包类型声明文件。 -#### 示例 - 如果你想要 [bundle 类型](/guide/advanced/dts#bundle-类型),你需要: 1. 安装 [@microsoft/api-extractor](https://www.npmjs.com/package/@microsoft/api-extractor) 作为开发依赖,它是用于打包类型声明文件的底层工具。 @@ -87,9 +85,42 @@ export default { }; ``` -#### 处理第三方依赖 +#### dts.bundle.bundledPackages + +- **类型:** `string[]` + +用于指定需要打包类型声明文件的依赖项,该配置将传递给 `@microsoft/api-extractor` 的 [bundledPackages](https://api-extractor.com/pages/configs/api-extractor_json/#bundledpackages) 配置项。 + +默认情况下,Rslib 会根据以下配置确定需要外部化的依赖项,详见 [处理第三方依赖](/guide/advanced/third-party-deps)。 + +- [autoExternal](/config/lib/auto-external) 配置 +- [output.externals](/config/rsbuild/output#outputexternals) 配置 + +那些没有被外部化的直接依赖项(在 `package.json` 中声明)会被添加到 `bundledPackages` 中,这些包的类型声明文件将会被打包到最终的产物中。 + +当默认行为不能满足需求时,可以通过 `dts.bundle.bundledPackages` 显式指定需要打包的依赖项。设置该配置后,将完全覆盖上述默认行为。 + +该配置通常用于打包传递依赖项(即直接依赖的依赖)。假设项目直接依赖 `foo`,而 `foo` 又依赖 `bar`,如果需要同时打包 `foo` 和 `bar` 的类型声明文件,可以如下配置: + +```ts title="rslib.config.ts" +export default { + lib: [ + { + format: 'esm', + dts: { + // [!code highlight:3] + bundle: { + bundledPackages: ['foo', 'bar'], + }, + }, + }, + ], +}; +``` -当我们打包类型声明文件时,我们需要指定哪些第三方包的类型需要被打包,可以参考 [处理第三方依赖](/guide/advanced/third-party-deps) 文档了解更多关于 externals 相关的配置。 +::: note +`bundledPackages` 可以使用 [minimatch](https://www.npmjs.com/package/minimatch) 语法配置 glob 表达式,但仅会匹配 `package.json` 中已声明的直接依赖项。 +::: ### dts.distPath