diff --git a/packages/plugin-dts/src/bundledPackages.ts b/packages/plugin-dts/src/bundledPackages.ts new file mode 100644 index 0000000000..3e6df23d0d --- /dev/null +++ b/packages/plugin-dts/src/bundledPackages.ts @@ -0,0 +1,123 @@ +import { logger, type RsbuildConfig } from '@rsbuild/core'; +import fs from 'node:fs'; +import { join } from 'node:path'; +import type { PluginDtsOptions } from './types/options'; + +const isObject = (obj: unknown): obj is Record => + Object.prototype.toString.call(obj) === '[object Object]'; + +export const DEFAULT_EXCLUDED_PACKAGES: string[] = ['@types/react']; + +type CalculateBundledPackagesOptions = { + cwd: string; + autoExternal: PluginDtsOptions['autoExternal']; + userExternals?: NonNullable['externals']; + overrideBundledPackages?: string[]; +}; + +// use !externals +export const calcBundledPackages = ( + options: CalculateBundledPackagesOptions, +): string[] => { + const { cwd, autoExternal, userExternals, overrideBundledPackages } = options; + + if (overrideBundledPackages) { + return overrideBundledPackages; + } + + let pkgJson: { + dependencies?: Record; + peerDependencies?: Record; + devDependencies?: Record; + optionalDependencies?: Record; + }; + + try { + const content = fs.readFileSync(join(cwd, 'package.json'), 'utf-8'); + pkgJson = JSON.parse(content); + } catch { + logger.warn( + 'The type of third-party packages will not be bundled due to read package.json failed', + ); + return []; + } + + const externalOptions = autoExternal + ? { + dependencies: true, + peerDependencies: true, + optionalDependencies: true, + devDependencies: false, + ...(autoExternal === true ? {} : autoExternal), + } + : { + dependencies: false, + peerDependencies: false, + optionalDependencies: false, + devDependencies: false, + }; + + // User externals should not bundled + // Only handle the case where the externals type is string / (string | RegExp)[] / plain object, function type is too complex. + const forcedBundledPackages = new Set(); + const getUserExternalsKeys = ( + value: typeof userExternals, + ): (string | RegExp)[] => { + if (!value) { + return []; + } + + if (typeof value === 'string' || value instanceof RegExp) { + return [value]; + } + + if (Array.isArray(value)) { + return value.flatMap((v) => getUserExternalsKeys(v)); + } + + if (isObject(value)) { + const keys: string[] = []; + for (const [key, external] of Object.entries(value)) { + if (external === false) { + forcedBundledPackages.add(key); + } else { + keys.push(key); + } + } + return keys; + } + return []; + }; + + const externals: (string | RegExp)[] = getUserExternalsKeys(userExternals); + + const allDeps: string[] = []; + + for (const type of [ + 'dependencies', + 'peerDependencies', + 'optionalDependencies', + 'devDependencies', + ] as const) { + const deps = pkgJson[type] && Object.keys(pkgJson[type]); + if (deps) { + if (externalOptions[type]) { + externals.push( + ...deps.filter((dep) => !forcedBundledPackages.has(dep)), + ); + } + allDeps.push(...deps); + } + } + + const bundledPackages = allDeps.filter( + (d) => + !externals.some((e) => (typeof e === 'string' ? d === e : e.test(d))), + ); + + const filteredBundledPackages = Array.from(new Set(bundledPackages)).filter( + (pkg) => !DEFAULT_EXCLUDED_PACKAGES.includes(pkg), + ); + + return filteredBundledPackages; +}; diff --git a/packages/plugin-dts/src/dts.ts b/packages/plugin-dts/src/dts.ts index eeeeb402e5..edd7a8b212 100644 --- a/packages/plugin-dts/src/dts.ts +++ b/packages/plugin-dts/src/dts.ts @@ -23,125 +23,6 @@ import { mergeAliasWithTsConfigPaths, } from './utils'; -const isObject = (obj: unknown): obj is Record => - Object.prototype.toString.call(obj) === '[object Object]'; - -export const DEFAULT_EXCLUDED_PACKAGES: string[] = ['@types/react']; - -type CalculateBundledPackagesOptions = { - cwd: string; - autoExternal: DtsGenOptions['autoExternal']; - userExternals?: DtsGenOptions['userExternals']; - overrideBundledPackages?: string[]; -}; - -// use !externals -export const calcBundledPackages = ( - options: CalculateBundledPackagesOptions, -): string[] => { - const { cwd, autoExternal, userExternals, overrideBundledPackages } = options; - - if (overrideBundledPackages) { - return overrideBundledPackages; - } - - let pkgJson: { - dependencies?: Record; - peerDependencies?: Record; - devDependencies?: Record; - optionalDependencies?: Record; - }; - - try { - const content = fs.readFileSync(join(cwd, 'package.json'), 'utf-8'); - pkgJson = JSON.parse(content); - } catch { - logger.warn( - 'The type of third-party packages will not be bundled due to read package.json failed', - ); - return []; - } - - const externalOptions = autoExternal - ? { - dependencies: true, - peerDependencies: true, - optionalDependencies: true, - devDependencies: false, - ...(autoExternal === true ? {} : autoExternal), - } - : { - dependencies: false, - peerDependencies: false, - optionalDependencies: false, - devDependencies: false, - }; - - // User externals should not bundled - // Only handle the case where the externals type is string / (string | RegExp)[] / plain object, function type is too complex. - const forcedBundledPackages = new Set(); - const getUserExternalsKeys = ( - value: typeof userExternals, - ): (string | RegExp)[] => { - if (!value) { - return []; - } - - if (typeof value === 'string' || value instanceof RegExp) { - return [value]; - } - - if (Array.isArray(value)) { - return value.flatMap((v) => getUserExternalsKeys(v)); - } - - if (isObject(value)) { - const keys: string[] = []; - for (const [key, external] of Object.entries(value)) { - if (external === false) { - forcedBundledPackages.add(key); - } else { - keys.push(key); - } - } - return keys; - } - return []; - }; - - const externals: (string | RegExp)[] = getUserExternalsKeys(userExternals); - - const allDeps: string[] = []; - - for (const type of [ - 'dependencies', - 'peerDependencies', - 'optionalDependencies', - 'devDependencies', - ] as const) { - const deps = pkgJson[type] && Object.keys(pkgJson[type]); - if (deps) { - if (externalOptions[type]) { - externals.push( - ...deps.filter((dep) => !forcedBundledPackages.has(dep)), - ); - } - allDeps.push(...deps); - } - } - - const bundledPackages = allDeps.filter( - (d) => - !externals.some((e) => (typeof e === 'string' ? d === e : e.test(d))), - ); - - const filteredBundledPackages = Array.from(new Set(bundledPackages)).filter( - (pkg) => !DEFAULT_EXCLUDED_PACKAGES.includes(pkg), - ); - - return filteredBundledPackages; -}; - export type PreparedDtsContext = { declarationDir: string; rootDir: string; @@ -282,9 +163,7 @@ export async function bundleDtsIfNeeded( tsconfigPath: emitTsconfigPath, bundleTsconfigPath, dtsExtension = '.d.ts', - autoExternal = true, - userExternals, - apiExtractorOptions, + bundledPackages, banner, footer, } = data; @@ -316,12 +195,7 @@ export async function bundleDtsIfNeeded( dtsExtension, banner, footer, - bundledPackages: calcBundledPackages({ - cwd, - autoExternal, - userExternals, - overrideBundledPackages: apiExtractorOptions?.bundledPackages, - }), + bundledPackages, }); } diff --git a/packages/plugin-dts/src/index.ts b/packages/plugin-dts/src/index.ts index cab8b68d7a..10b39a61ac 100644 --- a/packages/plugin-dts/src/index.ts +++ b/packages/plugin-dts/src/index.ts @@ -12,7 +12,7 @@ import { resolveDtsGenerationBackend, resolveTypescriptPath, } from './backend'; -import { calcBundledPackages } from './dts'; +import { calcBundledPackages } from './bundledPackages'; import { createIsolatedDtsContext, processIsolatedDts, @@ -246,6 +246,7 @@ export const pluginDts: (options?: PluginDtsOptions) => RsbuildPlugin = ( isolated: _isolated, tsgo: _tsgo, typescriptPath: _typescriptPath, + autoExternal = true, ...rest } = options; @@ -254,20 +255,16 @@ export const pluginDts: (options?: PluginDtsOptions) => RsbuildPlugin = ( bundle, dtsEntry, dtsEmitPath, - userExternals: config.output.externals, // RegExp externals are lost when the options are sent to the // declaration process, so resolve the bundled packages here. - apiExtractorOptions: bundle - ? { - ...apiExtractorOptions, - bundledPackages: calcBundledPackages({ - cwd, - autoExternal: options.autoExternal ?? true, - userExternals: config.output.externals, - overrideBundledPackages: apiExtractorOptions.bundledPackages, - }), - } - : apiExtractorOptions, + bundledPackages: bundle + ? calcBundledPackages({ + cwd, + autoExternal, + userExternals: config.output.externals, + overrideBundledPackages: apiExtractorOptions.bundledPackages, + }) + : [], tsconfigPath, bundleTsconfigPath: resolvedBundleTsconfigPath ?? tsconfigPath, tsConfigResult, diff --git a/packages/plugin-dts/src/types/internal.ts b/packages/plugin-dts/src/types/internal.ts index 85f183ce43..e780200ee1 100644 --- a/packages/plugin-dts/src/types/internal.ts +++ b/packages/plugin-dts/src/types/internal.ts @@ -1,6 +1,6 @@ -import type { LogLevel, RsbuildConfig } from '@rsbuild/core'; +import type { LogLevel } from '@rsbuild/core'; import type { ParsedCommandLine } from 'typescript6-api'; -import type { ApiExtractorOptions, PluginDtsOptions } from './options'; +import type { PluginDtsOptions } from './options'; export type DtsGenerationBackend = 'api-old' | 'api-new' | 'ts7-executable' | 'isolated'; @@ -19,7 +19,7 @@ export type DtsTsconfigResult = OldApiTsconfigResult | TsconfigOptionsResult; export type DtsGenOptions = Omit< PluginDtsOptions, - 'bundle' | 'isolated' | 'tsgo' + 'bundle' | 'isolated' | 'tsgo' | 'autoExternal' > & { bundle: boolean; name: string; @@ -32,8 +32,7 @@ export type DtsGenOptions = Omit< tsconfigPath: string; bundleTsconfigPath: string; tsConfigResult: DtsTsconfigResult; - userExternals?: NonNullable['externals']; - apiExtractorOptions?: ApiExtractorOptions; + bundledPackages: string[]; loggerLevel: LogLevel; dtsBackend: DtsGenerationBackend; }; diff --git a/packages/plugin-dts/tests/external.test.ts b/packages/plugin-dts/tests/external.test.ts index cb14c8f45b..0b8d4237ab 100644 --- a/packages/plugin-dts/tests/external.test.ts +++ b/packages/plugin-dts/tests/external.test.ts @@ -1,7 +1,10 @@ import { logger } from '@rsbuild/core'; import { describe, expect, it, rs } from '@rstest/core'; import fs from 'node:fs'; -import { DEFAULT_EXCLUDED_PACKAGES, calcBundledPackages } from '../src/dts'; +import { + DEFAULT_EXCLUDED_PACKAGES, + calcBundledPackages, +} from '../src/bundledPackages'; const defaultExcludePackages = DEFAULT_EXCLUDED_PACKAGES.reduce( (acc: Record, cur: string) => { diff --git a/tests/integration/dts/bundle/bundled-packages/rslib.config.ts b/tests/integration/dts/bundle/bundled-packages/rslib.config.ts index 682dc31596..4bdb4f8cb4 100644 --- a/tests/integration/dts/bundle/bundled-packages/rslib.config.ts +++ b/tests/integration/dts/bundle/bundled-packages/rslib.config.ts @@ -48,5 +48,16 @@ export default defineConfig({ distPath: './dist/esm/regexp-externals', }, }), + generateBundleEsmConfig({ + dts: { + bundle: { + bundledPackages: ['@reduxjs/toolkit'], + }, + }, + output: { + externals: [/^@reduxjs\/toolkit$/], + distPath: './dist/esm/bundled-packages-priority', + }, + }), ], }); diff --git a/tests/integration/dts/bundle/index.test.ts b/tests/integration/dts/bundle/index.test.ts index 1592ca9265..84f538064a 100644 --- a/tests/integration/dts/bundle/index.test.ts +++ b/tests/integration/dts/bundle/index.test.ts @@ -253,7 +253,7 @@ describe('dts when bundle: true', () => { restore(); }); - test('override with bundledPackages', async () => { + test('respects bundledPackages and externals', async () => { const fixturePath = join(__dirname, 'bundled-packages'); const { entries } = await buildAndGetResults({ fixturePath, @@ -277,5 +277,12 @@ describe('dts when bundle: true', () => { // RegExp externals expect(entries.esm3).toContain(`export * from "@reduxjs/toolkit";`); + expect(entries.esm3).not.toContain( + 'export declare function configureStore', + ); + + // Explicit bundledPackages takes precedence over RegExp externals. + expect(entries.esm4).toContain('export declare function configureStore'); + expect(entries.esm4).not.toContain(`export * from "@reduxjs/toolkit";`); }); });