Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions packages/plugin-dts/src/bundledPackages.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> =>
Object.prototype.toString.call(obj) === '[object Object]';

export const DEFAULT_EXCLUDED_PACKAGES: string[] = ['@types/react'];

type CalculateBundledPackagesOptions = {
cwd: string;
autoExternal: PluginDtsOptions['autoExternal'];
userExternals?: NonNullable<RsbuildConfig['output']>['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<string, string>;
peerDependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
};

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<string>();
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;
};
130 changes: 2 additions & 128 deletions packages/plugin-dts/src/dts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,125 +23,6 @@ import {
mergeAliasWithTsConfigPaths,
} from './utils';

const isObject = (obj: unknown): obj is Record<string, any> =>
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<string, string>;
peerDependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
};

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<string>();
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;
Expand Down Expand Up @@ -282,9 +163,7 @@ export async function bundleDtsIfNeeded(
tsconfigPath: emitTsconfigPath,
bundleTsconfigPath,
dtsExtension = '.d.ts',
autoExternal = true,
userExternals,
apiExtractorOptions,
bundledPackages,
banner,
footer,
} = data;
Expand Down Expand Up @@ -316,12 +195,7 @@ export async function bundleDtsIfNeeded(
dtsExtension,
banner,
footer,
bundledPackages: calcBundledPackages({
cwd,
autoExternal,
userExternals,
overrideBundledPackages: apiExtractorOptions?.bundledPackages,
}),
bundledPackages,
});
}

Expand Down
23 changes: 10 additions & 13 deletions packages/plugin-dts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
resolveDtsGenerationBackend,
resolveTypescriptPath,
} from './backend';
import { calcBundledPackages } from './dts';
import { calcBundledPackages } from './bundledPackages';
import {
createIsolatedDtsContext,
processIsolatedDts,
Expand Down Expand Up @@ -246,6 +246,7 @@ export const pluginDts: (options?: PluginDtsOptions) => RsbuildPlugin = (
isolated: _isolated,
tsgo: _tsgo,
typescriptPath: _typescriptPath,
autoExternal = true,
...rest
} = options;

Expand All @@ -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,
Expand Down
9 changes: 4 additions & 5 deletions packages/plugin-dts/src/types/internal.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -32,8 +32,7 @@ export type DtsGenOptions = Omit<
tsconfigPath: string;
bundleTsconfigPath: string;
tsConfigResult: DtsTsconfigResult;
userExternals?: NonNullable<RsbuildConfig['output']>['externals'];
apiExtractorOptions?: ApiExtractorOptions;
bundledPackages: string[];
loggerLevel: LogLevel;
dtsBackend: DtsGenerationBackend;
};
Expand Down
5 changes: 4 additions & 1 deletion packages/plugin-dts/tests/external.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>, cur: string) => {
Expand Down
11 changes: 11 additions & 0 deletions tests/integration/dts/bundle/bundled-packages/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
}),
],
});
9 changes: 8 additions & 1 deletion tests/integration/dts/bundle/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";`);
});
});
Loading