diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 3c5cadef3..ac109619a 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -930,6 +930,7 @@ const composeDtsConfig = async ( // Only setting ⁠dts.bundle to true will generate the bundled d.ts. bundle: dts?.bundle ?? false, distPath: dts?.distPath ?? output?.distPath?.root ?? './dist', + build: dts?.build ?? false, abortOnError: dts?.abortOnError ?? true, dtsExtension: dts?.autoExtension ? dtsExtension : '.d.ts', autoExternal, diff --git a/packages/core/src/types/config/index.ts b/packages/core/src/types/config/index.ts index 3f4247426..7b159240d 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config/index.ts @@ -29,7 +29,10 @@ export type Syntax = | string[]; export type Dts = - | (Pick & { + | (Pick< + PluginDtsOptions, + 'bundle' | 'distPath' | 'abortOnError' | 'build' + > & { autoExtension?: boolean; }) | boolean; diff --git a/packages/plugin-dts/src/dts.ts b/packages/plugin-dts/src/dts.ts index d689f335a..c77b4552c 100644 --- a/packages/plugin-dts/src/dts.ts +++ b/packages/plugin-dts/src/dts.ts @@ -110,6 +110,7 @@ export async function generateDts(data: DtsGenOptions): Promise { tsconfigPath, name, cwd, + build, isWatch, dtsExtension = '.d.ts', autoExternal = true, @@ -204,6 +205,7 @@ export async function generateDts(data: DtsGenOptions): Promise { onComplete, bundle, isWatch, + build, ); if (!isWatch) { diff --git a/packages/plugin-dts/src/index.ts b/packages/plugin-dts/src/index.ts index 362992cf8..502cc3021 100644 --- a/packages/plugin-dts/src/index.ts +++ b/packages/plugin-dts/src/index.ts @@ -10,6 +10,7 @@ const __dirname = dirname(__filename); export type PluginDtsOptions = { bundle?: boolean; distPath?: string; + build?: boolean; abortOnError?: boolean; dtsExtension?: string; autoExternal?: @@ -33,6 +34,7 @@ export type DtsGenOptions = PluginDtsOptions & { cwd: string; isWatch: boolean; dtsEntry: DtsEntry; + build?: boolean; tsconfigPath?: string; userExternals?: NonNullable['externals']; }; @@ -46,7 +48,6 @@ export const PLUGIN_DTS_NAME = 'rsbuild:dts'; // use ts compiler API to generate bundleless dts // use ts compiler API and api-extractor to generate dts bundle -// TODO: support incremental build, to build one or more projects and their dependencies // TODO: deal alias in dts export const pluginDts = (options: PluginDtsOptions): RsbuildPlugin => ({ name: PLUGIN_DTS_NAME, @@ -54,6 +55,7 @@ export const pluginDts = (options: PluginDtsOptions): RsbuildPlugin => ({ setup(api) { options.bundle = options.bundle ?? false; options.abortOnError = options.abortOnError ?? true; + options.build = options.build || false; const dtsPromises: Promise[] = []; let promisesResult: TaskResult[] = []; diff --git a/packages/plugin-dts/src/tsc.ts b/packages/plugin-dts/src/tsc.ts index 48e8529e6..96cdc0563 100644 --- a/packages/plugin-dts/src/tsc.ts +++ b/packages/plugin-dts/src/tsc.ts @@ -23,6 +23,7 @@ export async function emitDts( onComplete: (isSuccess: boolean) => void, bundle = false, isWatch = false, + build = false, ): Promise { const start = Date.now(); const { configPath, declarationDir, name, dtsExtension, banner, footer } = @@ -42,131 +43,172 @@ export async function emitDts( emitDeclarationOnly: true, }; - if (!isWatch) { - const host: ts.CompilerHost = ts.createCompilerHost(compilerOptions); - - const program: ts.Program = ts.createProgram({ - rootNames: fileNames, - options: compilerOptions, - projectReferences, - host, - configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics( - configFileParseResult, - ), - }); - - const emitResult = program.emit(); - - const allDiagnostics = ts - .getPreEmitDiagnostics(program) - .concat(emitResult.diagnostics); + const createProgram = ts.createSemanticDiagnosticsBuilderProgram; + const formatHost: ts.FormatDiagnosticsHost = { + getCanonicalFileName: (path) => path, + getCurrentDirectory: ts.sys.getCurrentDirectory, + getNewLine: () => ts.sys.newLine, + }; - const diagnosticMessages: string[] = []; + const reportDiagnostic = (diagnostic: ts.Diagnostic) => { + const fileLoc = getFileLoc(diagnostic, configPath); - for (const diagnostic of allDiagnostics) { - const fileLoc = getFileLoc(diagnostic, configPath); - const message = `${fileLoc} - ${color.red('error')} ${color.gray(`TS${diagnostic.code}:`)} ${ts.flattenDiagnosticMessageText( + logger.error( + `${fileLoc} - ${color.red('error')} ${color.gray(`TS${diagnostic.code}:`)}`, + ts.flattenDiagnosticMessageText( diagnostic.messageText, - host.getNewLine(), - )}`; - diagnosticMessages.push(message); - } - - await processDtsFiles(bundle, declarationDir, dtsExtension, banner, footer); + formatHost.getNewLine(), + ), + ); + }; - if (diagnosticMessages.length) { - logger.error( - `Failed to emit declaration files. ${color.gray(`(${name})`)}`, - ); + const reportWatchStatusChanged: ts.WatchStatusReporter = async ( + diagnostic: ts.Diagnostic, + _newLine: string, + _options: ts.CompilerOptions, + errorCount?: number, + ) => { + const message = `${ts.flattenDiagnosticMessageText( + diagnostic.messageText, + formatHost.getNewLine(), + )} ${color.gray(`(${name})`)}`; + + // 6031: File change detected. Starting incremental compilation... + // 6032: Starting compilation in watch mode... + if (diagnostic.code === 6031 || diagnostic.code === 6032) { + logger.info(message); + } - for (const message of diagnosticMessages) { + // 6194: 0 errors or 2+ errors! + if (diagnostic.code === 6194) { + if (errorCount === 0 || !errorCount) { + logger.info(message); + onComplete(true); + } else { logger.error(message); } + await processDtsFiles( + bundle, + declarationDir, + dtsExtension, + banner, + footer, + ); + } - throw new Error('DTS generation failed'); + // 6193: 1 error + if (diagnostic.code === 6193) { + logger.error(message); + await processDtsFiles( + bundle, + declarationDir, + dtsExtension, + banner, + footer, + ); } + }; - logger.ready( - `DTS generated in ${getTimeCost(start)} ${color.gray(`(${name})`)}`, - ); - } else { - const createProgram = ts.createSemanticDiagnosticsBuilderProgram; - const formatHost: ts.FormatDiagnosticsHost = { - getCanonicalFileName: (path) => path, - getCurrentDirectory: ts.sys.getCurrentDirectory, - getNewLine: () => ts.sys.newLine, - }; - - const reportDiagnostic = (diagnostic: ts.Diagnostic) => { - const fileLoc = getFileLoc(diagnostic, configPath); - - logger.error( - `${fileLoc} - ${color.red('error')} ${color.gray(`TS${diagnostic.code}:`)}`, - ts.flattenDiagnosticMessageText( - diagnostic.messageText, - formatHost.getNewLine(), + const system = { ...ts.sys }; + + if (!isWatch) { + // build mode + if (!build) { + const host: ts.CompilerHost = ts.createCompilerHost(compilerOptions); + + const program: ts.Program = ts.createProgram({ + rootNames: fileNames, + options: compilerOptions, + projectReferences, + host, + configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics( + configFileParseResult, ), - ); - }; - - const reportWatchStatusChanged: ts.WatchStatusReporter = async ( - diagnostic: ts.Diagnostic, - _newLine: string, - _options: ts.CompilerOptions, - errorCount?: number, - ) => { - const message = `${ts.flattenDiagnosticMessageText( - diagnostic.messageText, - formatHost.getNewLine(), - )} ${color.gray(`(${name})`)}`; + }); - // 6031: File change detected. Starting incremental compilation... - // 6032: Starting compilation in watch mode... - if (diagnostic.code === 6031 || diagnostic.code === 6032) { - logger.info(message); + const emitResult = program.emit(); + + const allDiagnostics = ts + .getPreEmitDiagnostics(program) + .concat(emitResult.diagnostics); + + const diagnosticMessages: string[] = []; + + for (const diagnostic of allDiagnostics) { + const fileLoc = getFileLoc(diagnostic, configPath); + const message = `${fileLoc} - ${color.red('error')} ${color.gray(`TS${diagnostic.code}:`)} ${ts.flattenDiagnosticMessageText( + diagnostic.messageText, + host.getNewLine(), + )}`; + diagnosticMessages.push(message); } - // 6194: 0 errors or 2+ errors! - if (diagnostic.code === 6194) { - if (errorCount === 0) { - logger.info(message); - onComplete(true); - } else { + await processDtsFiles( + bundle, + declarationDir, + dtsExtension, + banner, + footer, + ); + + if (diagnosticMessages.length) { + logger.error( + `Failed to emit declaration files. ${color.gray(`(${name})`)}`, + ); + + for (const message of diagnosticMessages) { logger.error(message); } - await processDtsFiles( - bundle, - declarationDir, - dtsExtension, - banner, - footer, - ); - } - // 6193: 1 error - if (diagnostic.code === 6193) { - logger.error(message); - await processDtsFiles( - bundle, - declarationDir, - dtsExtension, - banner, - footer, - ); + throw new Error('DTS generation failed'); } - }; + } else { + // incremental build with project references + const host = ts.createSolutionBuilderHost( + system, + createProgram, + reportDiagnostic, + ); - const system = { ...ts.sys }; + const solutionBuilder = ts.createSolutionBuilder(host, [configPath], {}); + + solutionBuilder.build(); + } - const host = ts.createWatchCompilerHost( - configPath, - compilerOptions, - system, - createProgram, - reportDiagnostic, - reportWatchStatusChanged, + logger.ready( + `DTS generated in ${getTimeCost(start)} ${color.gray(`(${name})`)}`, ); + } else { + // watch mode + if (!build) { + const host = ts.createWatchCompilerHost( + configPath, + compilerOptions, + system, + createProgram, + reportDiagnostic, + reportWatchStatusChanged, + ); + + ts.createWatchProgram(host); + } else { + // incremental build with project references + const host = ts.createSolutionBuilderWithWatchHost( + system, + createProgram, + reportDiagnostic, + undefined, + reportWatchStatusChanged, + ); - ts.createWatchProgram(host); + const solutionBuilder = ts.createSolutionBuilderWithWatch( + host, + [configPath], + {}, + { watch: true }, + ); + + solutionBuilder.build(); + } } } diff --git a/packages/plugin-dts/src/utils.ts b/packages/plugin-dts/src/utils.ts index f56eef55d..b85c1ece4 100644 --- a/packages/plugin-dts/src/utils.ts +++ b/packages/plugin-dts/src/utils.ts @@ -95,10 +95,17 @@ export async function addBannerAndFooter( const content = await fsP.readFile(file, 'utf-8'); const code = new MagicString(content); - banner && code.prepend(`${banner}\n`); - footer && code.append(`\n${footer}\n`); + if (banner && !content.trimStart().startsWith(banner.trim())) { + code.prepend(`${banner}\n`); + } + + if (footer && !content.trimEnd().endsWith(footer.trim())) { + code.append(`\n${footer}\n`); + } - await fsP.writeFile(file, code.toString()); + if (code.hasChanged()) { + await fsP.writeFile(file, code.toString()); + } } export async function processDtsFiles(