Skip to content

Commit 17c761c

Browse files
committed
feat: support tsc --build to incremental build project references
1 parent 9e8b683 commit 17c761c

File tree

6 files changed

+166
-109
lines changed

6 files changed

+166
-109
lines changed

packages/core/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,7 @@ const composeDtsConfig = async (
930930
// Only setting ⁠dts.bundle to true will generate the bundled d.ts.
931931
bundle: dts?.bundle ?? false,
932932
distPath: dts?.distPath ?? output?.distPath?.root ?? './dist',
933+
build: dts?.build ?? false,
933934
abortOnError: dts?.abortOnError ?? true,
934935
dtsExtension: dts?.autoExtension ? dtsExtension : '.d.ts',
935936
autoExternal,

packages/core/src/types/config/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export type Syntax =
2929
| string[];
3030

3131
export type Dts =
32-
| (Pick<PluginDtsOptions, 'bundle' | 'distPath' | 'abortOnError'> & {
32+
| (Pick<
33+
PluginDtsOptions,
34+
'bundle' | 'distPath' | 'abortOnError' | 'build'
35+
> & {
3336
autoExtension?: boolean;
3437
})
3538
| boolean;

packages/plugin-dts/src/dts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export async function generateDts(data: DtsGenOptions): Promise<void> {
110110
tsconfigPath,
111111
name,
112112
cwd,
113+
build,
113114
isWatch,
114115
dtsExtension = '.d.ts',
115116
autoExternal = true,
@@ -204,6 +205,7 @@ export async function generateDts(data: DtsGenOptions): Promise<void> {
204205
onComplete,
205206
bundle,
206207
isWatch,
208+
build,
207209
);
208210

209211
if (!isWatch) {

packages/plugin-dts/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const __dirname = dirname(__filename);
1010
export type PluginDtsOptions = {
1111
bundle?: boolean;
1212
distPath?: string;
13+
build?: boolean;
1314
abortOnError?: boolean;
1415
dtsExtension?: string;
1516
autoExternal?:
@@ -33,6 +34,7 @@ export type DtsGenOptions = PluginDtsOptions & {
3334
cwd: string;
3435
isWatch: boolean;
3536
dtsEntry: DtsEntry;
37+
build?: boolean;
3638
tsconfigPath?: string;
3739
userExternals?: NonNullable<RsbuildConfig['output']>['externals'];
3840
};
@@ -46,14 +48,14 @@ export const PLUGIN_DTS_NAME = 'rsbuild:dts';
4648

4749
// use ts compiler API to generate bundleless dts
4850
// use ts compiler API and api-extractor to generate dts bundle
49-
// TODO: support incremental build, to build one or more projects and their dependencies
5051
// TODO: deal alias in dts
5152
export const pluginDts = (options: PluginDtsOptions): RsbuildPlugin => ({
5253
name: PLUGIN_DTS_NAME,
5354

5455
setup(api) {
5556
options.bundle = options.bundle ?? false;
5657
options.abortOnError = options.abortOnError ?? true;
58+
options.build = options.build || false;
5759

5860
const dtsPromises: Promise<TaskResult>[] = [];
5961
let promisesResult: TaskResult[] = [];

packages/plugin-dts/src/tsc.ts

Lines changed: 146 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export async function emitDts(
2323
onComplete: (isSuccess: boolean) => void,
2424
bundle = false,
2525
isWatch = false,
26+
build = false,
2627
): Promise<void> {
2728
const start = Date.now();
2829
const { configPath, declarationDir, name, dtsExtension, banner, footer } =
@@ -42,131 +43,172 @@ export async function emitDts(
4243
emitDeclarationOnly: true,
4344
};
4445

45-
if (!isWatch) {
46-
const host: ts.CompilerHost = ts.createCompilerHost(compilerOptions);
47-
48-
const program: ts.Program = ts.createProgram({
49-
rootNames: fileNames,
50-
options: compilerOptions,
51-
projectReferences,
52-
host,
53-
configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(
54-
configFileParseResult,
55-
),
56-
});
57-
58-
const emitResult = program.emit();
59-
60-
const allDiagnostics = ts
61-
.getPreEmitDiagnostics(program)
62-
.concat(emitResult.diagnostics);
46+
const createProgram = ts.createSemanticDiagnosticsBuilderProgram;
47+
const formatHost: ts.FormatDiagnosticsHost = {
48+
getCanonicalFileName: (path) => path,
49+
getCurrentDirectory: ts.sys.getCurrentDirectory,
50+
getNewLine: () => ts.sys.newLine,
51+
};
6352

64-
const diagnosticMessages: string[] = [];
53+
const reportDiagnostic = (diagnostic: ts.Diagnostic) => {
54+
const fileLoc = getFileLoc(diagnostic, configPath);
6555

66-
for (const diagnostic of allDiagnostics) {
67-
const fileLoc = getFileLoc(diagnostic, configPath);
68-
const message = `${fileLoc} - ${color.red('error')} ${color.gray(`TS${diagnostic.code}:`)} ${ts.flattenDiagnosticMessageText(
56+
logger.error(
57+
`${fileLoc} - ${color.red('error')} ${color.gray(`TS${diagnostic.code}:`)}`,
58+
ts.flattenDiagnosticMessageText(
6959
diagnostic.messageText,
70-
host.getNewLine(),
71-
)}`;
72-
diagnosticMessages.push(message);
73-
}
74-
75-
await processDtsFiles(bundle, declarationDir, dtsExtension, banner, footer);
60+
formatHost.getNewLine(),
61+
),
62+
);
63+
};
7664

77-
if (diagnosticMessages.length) {
78-
logger.error(
79-
`Failed to emit declaration files. ${color.gray(`(${name})`)}`,
80-
);
65+
const reportWatchStatusChanged: ts.WatchStatusReporter = async (
66+
diagnostic: ts.Diagnostic,
67+
_newLine: string,
68+
_options: ts.CompilerOptions,
69+
errorCount?: number,
70+
) => {
71+
const message = `${ts.flattenDiagnosticMessageText(
72+
diagnostic.messageText,
73+
formatHost.getNewLine(),
74+
)} ${color.gray(`(${name})`)}`;
75+
76+
// 6031: File change detected. Starting incremental compilation...
77+
// 6032: Starting compilation in watch mode...
78+
if (diagnostic.code === 6031 || diagnostic.code === 6032) {
79+
logger.info(message);
80+
}
8181

82-
for (const message of diagnosticMessages) {
82+
// 6194: 0 errors or 2+ errors!
83+
if (diagnostic.code === 6194) {
84+
if (errorCount === 0 || !errorCount) {
85+
logger.info(message);
86+
onComplete(true);
87+
} else {
8388
logger.error(message);
8489
}
90+
await processDtsFiles(
91+
bundle,
92+
declarationDir,
93+
dtsExtension,
94+
banner,
95+
footer,
96+
);
97+
}
8598

86-
throw new Error('DTS generation failed');
99+
// 6193: 1 error
100+
if (diagnostic.code === 6193) {
101+
logger.error(message);
102+
await processDtsFiles(
103+
bundle,
104+
declarationDir,
105+
dtsExtension,
106+
banner,
107+
footer,
108+
);
87109
}
110+
};
88111

89-
logger.ready(
90-
`DTS generated in ${getTimeCost(start)} ${color.gray(`(${name})`)}`,
91-
);
92-
} else {
93-
const createProgram = ts.createSemanticDiagnosticsBuilderProgram;
94-
const formatHost: ts.FormatDiagnosticsHost = {
95-
getCanonicalFileName: (path) => path,
96-
getCurrentDirectory: ts.sys.getCurrentDirectory,
97-
getNewLine: () => ts.sys.newLine,
98-
};
99-
100-
const reportDiagnostic = (diagnostic: ts.Diagnostic) => {
101-
const fileLoc = getFileLoc(diagnostic, configPath);
102-
103-
logger.error(
104-
`${fileLoc} - ${color.red('error')} ${color.gray(`TS${diagnostic.code}:`)}`,
105-
ts.flattenDiagnosticMessageText(
106-
diagnostic.messageText,
107-
formatHost.getNewLine(),
112+
const system = { ...ts.sys };
113+
114+
if (!isWatch) {
115+
// build mode
116+
if (!build) {
117+
const host: ts.CompilerHost = ts.createCompilerHost(compilerOptions);
118+
119+
const program: ts.Program = ts.createProgram({
120+
rootNames: fileNames,
121+
options: compilerOptions,
122+
projectReferences,
123+
host,
124+
configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(
125+
configFileParseResult,
108126
),
109-
);
110-
};
111-
112-
const reportWatchStatusChanged: ts.WatchStatusReporter = async (
113-
diagnostic: ts.Diagnostic,
114-
_newLine: string,
115-
_options: ts.CompilerOptions,
116-
errorCount?: number,
117-
) => {
118-
const message = `${ts.flattenDiagnosticMessageText(
119-
diagnostic.messageText,
120-
formatHost.getNewLine(),
121-
)} ${color.gray(`(${name})`)}`;
127+
});
122128

123-
// 6031: File change detected. Starting incremental compilation...
124-
// 6032: Starting compilation in watch mode...
125-
if (diagnostic.code === 6031 || diagnostic.code === 6032) {
126-
logger.info(message);
129+
const emitResult = program.emit();
130+
131+
const allDiagnostics = ts
132+
.getPreEmitDiagnostics(program)
133+
.concat(emitResult.diagnostics);
134+
135+
const diagnosticMessages: string[] = [];
136+
137+
for (const diagnostic of allDiagnostics) {
138+
const fileLoc = getFileLoc(diagnostic, configPath);
139+
const message = `${fileLoc} - ${color.red('error')} ${color.gray(`TS${diagnostic.code}:`)} ${ts.flattenDiagnosticMessageText(
140+
diagnostic.messageText,
141+
host.getNewLine(),
142+
)}`;
143+
diagnosticMessages.push(message);
127144
}
128145

129-
// 6194: 0 errors or 2+ errors!
130-
if (diagnostic.code === 6194) {
131-
if (errorCount === 0) {
132-
logger.info(message);
133-
onComplete(true);
134-
} else {
146+
await processDtsFiles(
147+
bundle,
148+
declarationDir,
149+
dtsExtension,
150+
banner,
151+
footer,
152+
);
153+
154+
if (diagnosticMessages.length) {
155+
logger.error(
156+
`Failed to emit declaration files. ${color.gray(`(${name})`)}`,
157+
);
158+
159+
for (const message of diagnosticMessages) {
135160
logger.error(message);
136161
}
137-
await processDtsFiles(
138-
bundle,
139-
declarationDir,
140-
dtsExtension,
141-
banner,
142-
footer,
143-
);
144-
}
145162

146-
// 6193: 1 error
147-
if (diagnostic.code === 6193) {
148-
logger.error(message);
149-
await processDtsFiles(
150-
bundle,
151-
declarationDir,
152-
dtsExtension,
153-
banner,
154-
footer,
155-
);
163+
throw new Error('DTS generation failed');
156164
}
157-
};
165+
} else {
166+
// incremental build with project references
167+
const host = ts.createSolutionBuilderHost(
168+
system,
169+
createProgram,
170+
reportDiagnostic,
171+
);
158172

159-
const system = { ...ts.sys };
173+
const solutionBuilder = ts.createSolutionBuilder(host, [configPath], {});
174+
175+
solutionBuilder.build();
176+
}
160177

161-
const host = ts.createWatchCompilerHost(
162-
configPath,
163-
compilerOptions,
164-
system,
165-
createProgram,
166-
reportDiagnostic,
167-
reportWatchStatusChanged,
178+
logger.ready(
179+
`DTS generated in ${getTimeCost(start)} ${color.gray(`(${name})`)}`,
168180
);
181+
} else {
182+
// watch mode
183+
if (!build) {
184+
const host = ts.createWatchCompilerHost(
185+
configPath,
186+
compilerOptions,
187+
system,
188+
createProgram,
189+
reportDiagnostic,
190+
reportWatchStatusChanged,
191+
);
192+
193+
ts.createWatchProgram(host);
194+
} else {
195+
// incremental build with project references
196+
const host = ts.createSolutionBuilderWithWatchHost(
197+
system,
198+
createProgram,
199+
reportDiagnostic,
200+
undefined,
201+
reportWatchStatusChanged,
202+
);
169203

170-
ts.createWatchProgram(host);
204+
const solutionBuilder = ts.createSolutionBuilderWithWatch(
205+
host,
206+
[configPath],
207+
{},
208+
{ watch: true },
209+
);
210+
211+
solutionBuilder.build();
212+
}
171213
}
172214
}

packages/plugin-dts/src/utils.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,17 @@ export async function addBannerAndFooter(
9595
const content = await fsP.readFile(file, 'utf-8');
9696
const code = new MagicString(content);
9797

98-
banner && code.prepend(`${banner}\n`);
99-
footer && code.append(`\n${footer}\n`);
98+
if (banner && !content.trimStart().startsWith(banner.trim())) {
99+
code.prepend(`${banner}\n`);
100+
}
101+
102+
if (footer && !content.trimEnd().endsWith(footer.trim())) {
103+
code.append(`\n${footer}\n`);
104+
}
100105

101-
await fsP.writeFile(file, code.toString());
106+
if (code.hasChanged()) {
107+
await fsP.writeFile(file, code.toString());
108+
}
102109
}
103110

104111
export async function processDtsFiles(

0 commit comments

Comments
 (0)