diff --git a/e2e/cases/dts/__snapshots__/index.test.ts.snap b/e2e/cases/dts/__snapshots__/index.test.ts.snap index 099f93cc9..4bce88be6 100644 --- a/e2e/cases/dts/__snapshots__/index.test.ts.snap +++ b/e2e/cases/dts/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`dts when bundle: false > basic 1`] = ` +exports[`dts when bundle: false > basic 2`] = ` { "./dist/esm/index.d.ts": "export * from './utils/numbers'; export * from './utils/strings'; diff --git a/e2e/cases/dts/bundle-false/autoExtension.config.ts b/e2e/cases/dts/bundle-false/autoExtension.config.ts new file mode 100644 index 000000000..39499785f --- /dev/null +++ b/e2e/cases/dts/bundle-false/autoExtension.config.ts @@ -0,0 +1,21 @@ +import { generateBundleCjsConfig, generateBundleEsmConfig } from '@e2e/helper'; +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig(__dirname, { + bundle: false, + }), + generateBundleCjsConfig(__dirname, { + bundle: false, + dts: { + bundle: false, + }, + }), + ], + source: { + entry: { + main: ['./src/**'], + }, + }, +}); diff --git a/e2e/cases/dts/bundle-false/package.json b/e2e/cases/dts/bundle-false/package.json new file mode 100644 index 000000000..aa0ce962e --- /dev/null +++ b/e2e/cases/dts/bundle-false/package.json @@ -0,0 +1,6 @@ +{ + "name": "dts-bundle-false-test", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/e2e/cases/dts/bundle/autoExtension.config.ts b/e2e/cases/dts/bundle/autoExtension.config.ts new file mode 100644 index 000000000..c58dcca06 --- /dev/null +++ b/e2e/cases/dts/bundle/autoExtension.config.ts @@ -0,0 +1,18 @@ +import { generateBundleCjsConfig, generateBundleEsmConfig } from '@e2e/helper'; +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [ + generateBundleEsmConfig(__dirname), + generateBundleCjsConfig(__dirname, { + dts: { + bundle: true, + }, + }), + ], + source: { + entry: { + main: './src/index.ts', + }, + }, +}); diff --git a/e2e/cases/dts/index.test.ts b/e2e/cases/dts/index.test.ts index 7c7cad48b..08a8f5ae2 100644 --- a/e2e/cases/dts/index.test.ts +++ b/e2e/cases/dts/index.test.ts @@ -11,8 +11,14 @@ describe('dts when bundle: false', () => { 'dts', ); - expect(files.esm?.length).toBe(4); - expect(files.esm?.[0]!.endsWith('.d.ts')).toEqual(true); + expect(files.esm).toMatchInlineSnapshot(` + [ + "./dist/esm/index.d.ts", + "./dist/esm/sum.d.ts", + "./dist/esm/utils/numbers.d.ts", + "./dist/esm/utils/strings.d.ts", + ] + `); expect(contents.esm).toMatchSnapshot(); }); @@ -34,8 +40,15 @@ describe('dts when bundle: false', () => { 'distPath.config.ts', 'dts', ); - expect(files.esm?.length).toBe(4); - expect(files.esm?.[0]!.startsWith('./dist/custom')).toEqual(true); + + expect(files.esm).toMatchInlineSnapshot(` + [ + "./dist/custom/index.d.ts", + "./dist/custom/sum.d.ts", + "./dist/custom/utils/numbers.d.ts", + "./dist/custom/utils/strings.d.ts", + ] + `); }); test('abortOnError: false', async () => { @@ -48,6 +61,24 @@ describe('dts when bundle: false', () => { expect(isSuccess).toBe(true); }); + + test('autoExtension: true', async () => { + const fixturePath = join(__dirname, 'bundle-false'); + const { files } = await buildAndGetResults( + fixturePath, + 'autoExtension.config.ts', + 'dts', + ); + + expect(files.cjs).toMatchInlineSnapshot(` + [ + "./dist/cjs/index.d.cts", + "./dist/cjs/sum.d.cts", + "./dist/cjs/utils/numbers.d.cts", + "./dist/cjs/utils/strings.d.cts", + ] + `); + }); }); describe('dts when bundle: true', () => { @@ -59,7 +90,7 @@ describe('dts when bundle: true', () => { 'dts', ); - expect(entryFiles.esm!.endsWith('index.d.ts')).toEqual(true); + expect(entryFiles.esm).toEqual('./dist/esm/index.d.ts'); expect(entries).toMatchSnapshot(); }); @@ -82,7 +113,7 @@ describe('dts when bundle: true', () => { 'dts', ); - expect(entryFiles.esm!.startsWith('./dist/custom')).toEqual(true); + expect(entryFiles.esm).toEqual('./dist/custom/index.d.ts'); }); test('abortOnError: false', async () => { @@ -95,4 +126,15 @@ describe('dts when bundle: true', () => { expect(isSuccess).toBe(true); }); + + test('autoExtension: true', async () => { + const fixturePath = join(__dirname, 'bundle'); + const { entryFiles } = await buildAndGetResults( + fixturePath, + 'autoExtension.config.ts', + 'dts', + ); + + expect(entryFiles.cjs).toEqual('./dist/cjs/index.d.cts'); + }); }); diff --git a/examples/express-plugin/rslib.config.ts b/examples/express-plugin/rslib.config.ts index 3d5b07565..f4d96ea59 100644 --- a/examples/express-plugin/rslib.config.ts +++ b/examples/express-plugin/rslib.config.ts @@ -1,7 +1,6 @@ import { defineConfig } from '@rslib/core'; const shared = { - autoExtension: true, dts: { bundle: false, }, diff --git a/examples/react-component/rslib.config.ts b/examples/react-component/rslib.config.ts index db1d311d7..d97011009 100644 --- a/examples/react-component/rslib.config.ts +++ b/examples/react-component/rslib.config.ts @@ -2,7 +2,6 @@ import { pluginReact } from '@rsbuild/plugin-react'; import { defineConfig } from '@rslib/core'; const shared = { - autoExtension: true, dts: { bundle: false, }, diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 6017a7410..e39f100bc 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -168,19 +168,25 @@ const composeAutoExtensionConfig = ( format: Format, root: string, autoExtension: boolean, -): RsbuildConfig => { - const { jsExtension } = getDefaultExtension({ +): { + config: RsbuildConfig; + dtsExtension: string; +} => { + const { jsExtension, dtsExtension } = getDefaultExtension({ format, root, autoExtension, }); return { - output: { - filename: { - js: `[name]${jsExtension}`, + config: { + output: { + filename: { + js: `[name]${jsExtension}`, + }, }, }, + dtsExtension, }; }; @@ -325,6 +331,7 @@ const composeBundleConfig = (bundle = true): RsbuildConfig => { const composeDtsConfig = async ( libConfig: LibConfig, + dtsExtension: string, ): Promise => { const { dts, bundle, output } = libConfig; @@ -337,6 +344,7 @@ const composeDtsConfig = async ( bundle: dts?.bundle ?? bundle, distPath: dts?.distPath ?? output?.distPath?.root ?? './dist', abortOnError: dts?.abortOnError ?? true, + dtsExtension, }), ], }; @@ -389,11 +397,8 @@ async function composeLibRsbuildConfig( const { format, autoExtension = true } = config; const formatConfig = composeFormatConfig(format!); - const autoExtensionConfig = composeAutoExtensionConfig( - format!, - dirname(configPath), - autoExtension, - ); + const { config: autoExtensionConfig, dtsExtension } = + composeAutoExtensionConfig(format!, dirname(configPath), autoExtension); const bundleConfig = composeBundleConfig(config.bundle); const targetConfig = composeTargetConfig(config.output?.target); const syntaxConfig = composeSyntaxConfig( @@ -405,7 +410,7 @@ async function composeLibRsbuildConfig( config.bundle, dirname(configPath), ); - const dtsConfig = await composeDtsConfig(config); + const dtsConfig = await composeDtsConfig(config, dtsExtension); return mergeRsbuildConfig( formatConfig, diff --git a/packages/plugin-dts/package.json b/packages/plugin-dts/package.json index 7e3523e03..e7fd94536 100644 --- a/packages/plugin-dts/package.json +++ b/packages/plugin-dts/package.json @@ -30,6 +30,7 @@ "dev": "modern build --watch" }, "dependencies": { + "fast-glob": "^3.3.2", "picocolors": "1.0.1" }, "devDependencies": { diff --git a/packages/plugin-dts/src/apiExtractor.ts b/packages/plugin-dts/src/apiExtractor.ts index 58a5ec4bd..a92e1a00a 100644 --- a/packages/plugin-dts/src/apiExtractor.ts +++ b/packages/plugin-dts/src/apiExtractor.ts @@ -5,23 +5,34 @@ import { type ExtractorResult, } from '@microsoft/api-extractor'; import { logger } from '@rsbuild/core'; +import color from 'picocolors'; +import { getTimeCost } from './utils'; export type BundleOptions = { + name: string; cwd: string; outDir: string; + dtsExtension: string; entry?: string; tsconfigPath?: string; }; -export function bundleDts(options: BundleOptions): void { +export async function bundleDts(options: BundleOptions): Promise { const { + name, cwd, outDir, + dtsExtension, entry = 'index.d.ts', tsconfigPath = 'tsconfig.json', } = options; try { - const untrimmedFilePath = join(cwd, relative(cwd, outDir), 'index.d.ts'); + const start = Date.now(); + const untrimmedFilePath = join( + cwd, + relative(cwd, outDir), + `index${dtsExtension}`, + ); const internalConfig = { mainEntryPointFilePath: entry, // TODO: use !externals @@ -49,11 +60,11 @@ export function bundleDts(options: BundleOptions): void { }); if (!extractorResult.succeeded) { - throw new Error('API Extractor rollup error'); + throw new Error(`API Extractor error. ${color.gray(`(${name})`)}`); } logger.info( - `API Extractor writing package typings succeeded: ${untrimmedFilePath}`, + `API Extractor bundle DTS succeeded: ${color.cyan(untrimmedFilePath)} in ${getTimeCost(start)} ${color.gray(`(${name})`)}`, ); } catch (e) { logger.error('API Extractor', e); diff --git a/packages/plugin-dts/src/dts.ts b/packages/plugin-dts/src/dts.ts index aa4cb59d3..d6babe2e2 100644 --- a/packages/plugin-dts/src/dts.ts +++ b/packages/plugin-dts/src/dts.ts @@ -7,8 +7,16 @@ import { emitDts } from './tsc'; import { ensureTempDeclarationDir, loadTsconfig } from './utils'; export async function generateDts(data: DtsGenOptions): Promise { - const { tsconfigPath, distPath, bundle, entryPath, cwd, isWatch, name } = - data; + const { + bundle, + distPath, + entryPath, + tsconfigPath, + name, + cwd, + isWatch, + dtsExtension = '.d.ts', + } = data; logger.start(`Generating DTS... ${color.gray(`(${name})`)}`); const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, tsconfigPath); if (!configPath) { @@ -47,11 +55,13 @@ export async function generateDts(data: DtsGenOptions): Promise { const bundleDtsIfNeeded = async () => { if (bundle === true) { const { bundleDts } = await import('./apiExtractor'); - bundleDts({ + await bundleDts({ + name, cwd, outDir, entry, tsconfigPath, + dtsExtension, }); } }; @@ -62,15 +72,17 @@ export async function generateDts(data: DtsGenOptions): Promise { } }; - emitDts( + await emitDts( { name, cwd, configPath, rootDir, declarationDir, + dtsExtension, }, onComplete, + bundle, isWatch, ); diff --git a/packages/plugin-dts/src/index.ts b/packages/plugin-dts/src/index.ts index c9fe0fad1..648c506bd 100644 --- a/packages/plugin-dts/src/index.ts +++ b/packages/plugin-dts/src/index.ts @@ -6,6 +6,7 @@ export type PluginDtsOptions = { bundle?: boolean; distPath?: string; abortOnError?: boolean; + dtsExtension?: string; }; export type DtsGenOptions = PluginDtsOptions & { diff --git a/packages/plugin-dts/src/tsc.ts b/packages/plugin-dts/src/tsc.ts index bc8584d10..3ee73b0fa 100644 --- a/packages/plugin-dts/src/tsc.ts +++ b/packages/plugin-dts/src/tsc.ts @@ -1,7 +1,12 @@ import { logger } from '@rsbuild/core'; import color from 'picocolors'; import * as ts from 'typescript'; -import { getFileLoc, loadTsconfig } from './utils'; +import { + getFileLoc, + getTimeCost, + loadTsconfig, + processDtsFiles, +} from './utils'; export type emitDtsOptions = { name: string; @@ -9,18 +14,17 @@ export type emitDtsOptions = { configPath: string; rootDir: string; declarationDir: string; + dtsExtension: string; }; -export function emitDts( +export async function emitDts( options: emitDtsOptions, onComplete: (isSuccess: boolean) => void, + bundle = false, isWatch = false, -): void { +): Promise { const start = Date.now(); - const getTimeCost = () => { - return `${Math.floor(Date.now() - start)}ms`; - }; - const { configPath, declarationDir, name } = options; + const { configPath, declarationDir, name, dtsExtension } = options; const { options: rawCompilerOptions, fileNames } = loadTsconfig(configPath); const compilerOptions = { @@ -57,6 +61,8 @@ export function emitDts( diagnosticMessages.push(message); } + await processDtsFiles(bundle, declarationDir, dtsExtension); + if (diagnosticMessages.length) { logger.error( `Failed to emit declaration files. ${color.gray(`(${name})`)}`, @@ -70,7 +76,7 @@ export function emitDts( } logger.info( - `DTS generation succeeded in ${getTimeCost()} ${color.gray(`(${name})`)}`, + `DTS generation succeeded in ${getTimeCost(start)} ${color.gray(`(${name})`)}`, ); } else { const createProgram = ts.createSemanticDiagnosticsBuilderProgram; @@ -92,7 +98,7 @@ export function emitDts( ); }; - const reportWatchStatusChanged: ts.WatchStatusReporter = ( + const reportWatchStatusChanged: ts.WatchStatusReporter = async ( diagnostic: ts.Diagnostic, _newLine: string, _options: ts.CompilerOptions, @@ -117,11 +123,13 @@ export function emitDts( } else { logger.error(message); } + await processDtsFiles(bundle, declarationDir, dtsExtension); } // 6193: 1 error if (diagnostic.code === 6193) { logger.error(message); + await processDtsFiles(bundle, declarationDir, dtsExtension); } }; diff --git a/packages/plugin-dts/src/utils.ts b/packages/plugin-dts/src/utils.ts index 90168c4ff..320adef90 100644 --- a/packages/plugin-dts/src/utils.ts +++ b/packages/plugin-dts/src/utils.ts @@ -1,5 +1,7 @@ -import fs, { writeFileSync } from 'node:fs'; +import fs from 'node:fs'; import path from 'node:path'; +import { logger } from '@rsbuild/core'; +import fg from 'fast-glob'; import color from 'picocolors'; import * as ts from 'typescript'; @@ -27,7 +29,7 @@ export function ensureTempDeclarationDir(cwd: string): string { fs.mkdirSync(dirPath, { recursive: true }); const gitIgnorePath = path.join(cwd, `${TEMP_FOLDER}/.gitignore`); - writeFileSync(gitIgnorePath, '**/*\n'); + fs.writeFileSync(gitIgnorePath, '**/*\n'); return dirPath; } @@ -43,3 +45,28 @@ export function getFileLoc(diagnostic: ts.Diagnostic): string { return ''; } + +export function getTimeCost(start: number): string { + return `${Math.floor(Date.now() - start)}ms`; +} + +export async function processDtsFiles( + bundle: boolean, + dir: string, + dtsExtension: string, +): Promise { + if (bundle) { + return; + } + + const dtsFiles = await fg(`${dir}/**/*.d.ts`); + + for (const file of dtsFiles) { + try { + const newFile = file.replace('.d.ts', dtsExtension); + fs.renameSync(file, newFile); + } catch (error) { + logger.error(`Error renaming DTS file ${file}: ${error}`); + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4f9cbab7..7ab6872ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,8 @@ importers: e2e/cases/dts/bundle: {} + e2e/cases/dts/bundle-false: {} + e2e/scripts: {} examples/express-plugin: @@ -177,6 +179,9 @@ importers: packages/plugin-dts: dependencies: + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 picocolors: specifier: 1.0.1 version: 1.0.1