Skip to content

Commit 5bba57e

Browse files
committed
Cache dts emit results
Fixes #43995
1 parent 46e3513 commit 5bba57e

File tree

8 files changed

+188
-86
lines changed

8 files changed

+188
-86
lines changed

src/compiler/builder.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1173,7 +1173,7 @@ namespace ts {
11731173
const info = state.fileInfos.get(file.resolvedPath)!;
11741174
const signature = state.currentAffectedFilesSignatures?.get(file.resolvedPath) || info.signature;
11751175
if (signature === file.version) {
1176-
const newSignature = (computeHash || generateDjb2Hash)(data?.sourceMapUrlPos !== undefined ? text.substring(0, data.sourceMapUrlPos) : text);
1176+
const newSignature = BuilderState.getSignatureFromWriteFileText(text, data, computeHash);
11771177
if (newSignature !== file.version) { // Update it
11781178
if (host.storeFilesChangingSignatureDuringEmit) (state.filesChangingSignature ||= new Set()).add(file.resolvedPath);
11791179
if (state.exportedModulesMap) BuilderState.updateExportedModules(file, file.exportedModulesFromDeclarationEmit, state.currentAffectedFilesExportedModulesMap ||= BuilderState.createManyToManyPathMap());

src/compiler/builderState.ts

+16-13
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ namespace ts {
33
export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean,
44
cancellationToken?: CancellationToken, customTransformers?: CustomTransformers, forceDtsEmit?: boolean): EmitOutput {
55
const outputFiles: OutputFile[] = [];
6-
const { emitSkipped, diagnostics, exportedModulesFromDeclarationEmit } = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, forceDtsEmit);
7-
return { outputFiles, emitSkipped, diagnostics, exportedModulesFromDeclarationEmit };
6+
const { emitSkipped, diagnostics } = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers, forceDtsEmit);
7+
return { outputFiles, emitSkipped, diagnostics };
88

99
function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) {
1010
outputFiles.push({ name: fileName, writeByteOrderMark, text });
@@ -394,6 +394,10 @@ namespace ts {
394394
state.hasCalledUpdateShapeSignature.add(path);
395395
}
396396

397+
export function getSignatureFromWriteFileText(text: string, data: WriteFileCallbackData | undefined, computeHash: ComputeHash | undefined) {
398+
return (computeHash || generateDjb2Hash)(data?.sourceMapUrlPos !== undefined ? text.substring(0, data.sourceMapUrlPos) : text);
399+
}
400+
397401
/**
398402
* Returns if the shape of the signature has changed since last emit
399403
*/
@@ -412,22 +416,21 @@ namespace ts {
412416
const prevSignature = info.signature;
413417
let latestSignature: string | undefined;
414418
if (!sourceFile.isDeclarationFile && !useFileVersionAsSignature) {
415-
const emitOutput = getFileEmitOutput(
416-
programOfThisState,
419+
programOfThisState.emit(
417420
sourceFile,
418-
/*emitOnlyDtsFiles*/ true,
421+
(fileName, text, _writeByteOrderMark, _onError, _sourceFiles, data) => {
422+
Debug.assert(isDeclarationFileName(fileName), "File extension for signature expected to be dts");
423+
Debug.assert(latestSignature === undefined, "Only one file should be set");
424+
latestSignature = getSignatureFromWriteFileText(text, data, computeHash);
425+
if (exportedModulesMapCache && latestSignature !== prevSignature) {
426+
updateExportedModules(sourceFile, data?.exportedModulesFromDeclarationEmit, exportedModulesMapCache);
427+
}
428+
},
419429
cancellationToken,
430+
/*emitOnlyDtsFiles*/ true,
420431
/*customTransformers*/ undefined,
421432
/*forceDtsEmit*/ true
422433
);
423-
const firstDts = firstOrUndefined(emitOutput.outputFiles);
424-
if (firstDts) {
425-
Debug.assert(isDeclarationFileName(firstDts.name), "File extension for signature expected to be dts", () => `Found: ${getAnyExtensionFromPath(firstDts.name)} for ${firstDts.name}:: All output files: ${JSON.stringify(emitOutput.outputFiles.map(f => f.name))}`);
426-
latestSignature = (computeHash || generateDjb2Hash)(firstDts.text);
427-
if (exportedModulesMapCache && latestSignature !== prevSignature) {
428-
updateExportedModules(sourceFile, emitOutput.exportedModulesFromDeclarationEmit, exportedModulesMapCache);
429-
}
430-
}
431434
}
432435
// Default is to use file version as signature
433436
if (latestSignature === undefined) {

src/compiler/builderStatePublic.ts

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ namespace ts {
33
outputFiles: OutputFile[];
44
emitSkipped: boolean;
55
/* @internal */ diagnostics: readonly Diagnostic[];
6-
/* @internal */ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit;
76
}
87

98
export interface OutputFile {

src/compiler/emitter.ts

+86-20
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,15 @@ namespace ts {
276276
return Debug.fail(`project ${configFile.options.configFilePath} expected to have at least one output`);
277277
}
278278

279+
/*@internal*/
280+
export function getCacheDtsEmitResultKey(sourceFileOrBundle: SourceFile | Bundle, options: CompilerOptions) {
281+
return isSourceFile(sourceFileOrBundle) ? sourceFileOrBundle.resolvedPath : outFile(options) as Path;
282+
}
283+
284+
function isForceDtsEmitResult(result: CacheDtsEmitResult | undefined): result is ForceDtsEmitResult {
285+
return !!result && !(result as TransformationResult<SourceFile | Bundle>).transformed;
286+
}
287+
279288
/*@internal*/
280289
// targetSourceFile is when users only want one file in entire project to be emitted. This is used in compileOnSave feature
281290
export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile | undefined, { scriptTransformers, declarationTransformers }: EmitTransformers, emitOnlyDtsFiles?: boolean, onlyBuildInfo?: boolean, forceDtsEmit?: boolean): EmitResult {
@@ -288,7 +297,6 @@ namespace ts {
288297
const { enter, exit } = performance.createTimer("printTime", "beforePrint", "afterPrint");
289298
let bundleBuildInfo: BundleBuildInfo | undefined;
290299
let emitSkipped = false;
291-
let exportedModulesFromDeclarationEmit: ExportedModulesFromDeclarationEmit | undefined;
292300

293301
// Emit each output file
294302
enter();
@@ -308,7 +316,6 @@ namespace ts {
308316
diagnostics: emitterDiagnostics.getDiagnostics(),
309317
emittedFiles: emittedFilesList,
310318
sourceMaps: sourceMapDataList,
311-
exportedModulesFromDeclarationEmit
312319
};
313320

314321
function emitSourceFileOrBundle({ jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, buildInfoPath }: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle | undefined) {
@@ -411,8 +418,7 @@ namespace ts {
411418
substituteNode: transform.substituteNode,
412419
});
413420

414-
Debug.assert(transform.transformed.length === 1, "Should only see one output from the transform");
415-
printSourceFileOrBundle(jsFilePath, sourceMapFilePath, transform.transformed[0], printer, compilerOptions);
421+
printSourceFileOrBundle(jsFilePath, sourceMapFilePath, transform, printer, compilerOptions);
416422

417423
// Clean up emit nodes on parse tree
418424
transform.dispose();
@@ -433,12 +439,48 @@ namespace ts {
433439
const filesForEmit = forceDtsEmit ? sourceFiles : filter(sourceFiles, isSourceFileNotJson);
434440
// Setup and perform the transformation to retrieve declarations from the input files
435441
const inputListOrBundle = outFile(compilerOptions) ? [factory.createBundle(filesForEmit, !isSourceFile(sourceFileOrBundle) ? sourceFileOrBundle.prepends : undefined)] : filesForEmit;
436-
if (emitOnlyDtsFiles && !getEmitDeclarations(compilerOptions)) {
442+
// Use existing result:
443+
const key = getCacheDtsEmitResultKey(sourceFileOrBundle, compilerOptions);
444+
const cache = host.getDtsEmitResultCache();
445+
const existing = declarationTransformers.length === 1 ? cache.get(key) : undefined;
446+
if (isForceDtsEmitResult(existing)) {
447+
if (existing.diagnostics?.length) {
448+
for (const diagnostic of existing.diagnostics) {
449+
emitterDiagnostics.add(diagnostic);
450+
}
451+
}
452+
const declBlocked = !!existing.diagnostics?.length || !!host.isEmitBlocked(declarationFilePath) || !!compilerOptions.noEmit;
453+
emitSkipped = emitSkipped || declBlocked;
454+
if (!declBlocked || forceDtsEmit) {
455+
// Write the output file
456+
if (declarationMapPath && !forceDtsEmit) {
457+
if (sourceMapDataList && existing.sourceMapData) {
458+
sourceMapDataList.push(existing.sourceMapData);
459+
}
460+
writeFile(host, emitterDiagnostics, declarationMapPath, existing.sourceMap!, /*writeByteOrderMark*/ false, sourceFiles);
461+
}
462+
writeFile(
463+
host,
464+
emitterDiagnostics,
465+
declarationFilePath,
466+
existing.text,
467+
!!compilerOptions.emitBOM,
468+
filesForEmit,
469+
{
470+
sourceMapUrlPos: existing.sourceMapUrlPos,
471+
exportedModulesFromDeclarationEmit: existing.exportedModulesFromDeclarationEmit,
472+
}
473+
);
474+
}
475+
return;
476+
}
477+
478+
if (!existing && emitOnlyDtsFiles && !getEmitDeclarations(compilerOptions)) {
437479
// Checker wont collect the linked aliases since thats only done when declaration is enabled.
438480
// Do that here when emitting only dts files
439481
filesForEmit.forEach(collectLinkedAliases);
440482
}
441-
const declarationTransform = transformNodes(resolver, host, factory, compilerOptions, inputListOrBundle, declarationTransformers, /*allowDtsFiles*/ false);
483+
const declarationTransform = existing || transformNodes(resolver, host, factory, compilerOptions, inputListOrBundle, declarationTransformers, /*allowDtsFiles*/ false);
442484
if (length(declarationTransform.diagnostics)) {
443485
for (const diagnostic of declarationTransform.diagnostics!) {
444486
emitterDiagnostics.add(diagnostic);
@@ -451,8 +493,8 @@ namespace ts {
451493
noEmitHelpers: true,
452494
module: compilerOptions.module,
453495
target: compilerOptions.target,
454-
sourceMap: compilerOptions.sourceMap,
455-
inlineSourceMap: compilerOptions.inlineSourceMap,
496+
// Emit maps when there is noEmit not set with forceDtsEmit
497+
sourceMap: (!forceDtsEmit || !compilerOptions.noEmit) && compilerOptions.declarationMap,
456498
extendedDiagnostics: compilerOptions.extendedDiagnostics,
457499
onlyPrintJsDocStyle: true,
458500
writeBundleFileInfo: !!bundleBuildInfo,
@@ -472,24 +514,19 @@ namespace ts {
472514
const declBlocked = (!!declarationTransform.diagnostics && !!declarationTransform.diagnostics.length) || !!host.isEmitBlocked(declarationFilePath) || !!compilerOptions.noEmit;
473515
emitSkipped = emitSkipped || declBlocked;
474516
if (!declBlocked || forceDtsEmit) {
475-
Debug.assert(declarationTransform.transformed.length === 1, "Should only see one output from the decl transform");
476517
printSourceFileOrBundle(
477518
declarationFilePath,
478519
declarationMapPath,
479-
declarationTransform.transformed[0],
520+
declarationTransform,
480521
declarationPrinter,
481522
{
482-
sourceMap: !forceDtsEmit && compilerOptions.declarationMap,
523+
sourceMap: printerOptions.sourceMap,
483524
sourceRoot: compilerOptions.sourceRoot,
484525
mapRoot: compilerOptions.mapRoot,
485526
extendedDiagnostics: compilerOptions.extendedDiagnostics,
486527
// Explicitly do not passthru either `inline` option
487528
}
488529
);
489-
if (forceDtsEmit && declarationTransform.transformed[0].kind === SyntaxKind.SourceFile) {
490-
const sourceFile = declarationTransform.transformed[0];
491-
exportedModulesFromDeclarationEmit = sourceFile.exportedModulesFromDeclarationEmit;
492-
}
493530
}
494531
declarationTransform.dispose();
495532
if (bundleBuildInfo) bundleBuildInfo.dts = declarationPrinter.bundleFileInfo;
@@ -509,7 +546,10 @@ namespace ts {
509546
forEachChild(node, collectLinkedAliases);
510547
}
511548

512-
function printSourceFileOrBundle(jsFilePath: string, sourceMapFilePath: string | undefined, sourceFileOrBundle: SourceFile | Bundle, printer: Printer, mapOptions: SourceMapOptions) {
549+
function printSourceFileOrBundle(
550+
jsFilePath: string, sourceMapFilePath: string | undefined, transform: TransformationResult<SourceFile | Bundle>, printer: Printer, mapOptions: SourceMapOptions) {
551+
Debug.assert(transform.transformed.length === 1, "Should only see one output from the transform");
552+
const sourceFileOrBundle = transform.transformed[0];
513553
const bundle = sourceFileOrBundle.kind === SyntaxKind.Bundle ? sourceFileOrBundle : undefined;
514554
const sourceFile = sourceFileOrBundle.kind === SyntaxKind.SourceFile ? sourceFileOrBundle : undefined;
515555
const sourceFiles = bundle ? bundle.sourceFiles : [sourceFile!];
@@ -532,9 +572,11 @@ namespace ts {
532572
}
533573

534574
let sourceMapUrlPos;
575+
let sourceMap;
576+
let sourceMapData;
535577
if (sourceMapGenerator) {
536578
if (sourceMapDataList) {
537-
sourceMapDataList.push({
579+
sourceMapDataList.push(sourceMapData = {
538580
inputSourceFileNames: sourceMapGenerator.getSources(),
539581
sourceMap: sourceMapGenerator.toJSON()
540582
});
@@ -555,16 +597,39 @@ namespace ts {
555597

556598
// Write the source map
557599
if (sourceMapFilePath) {
558-
const sourceMap = sourceMapGenerator.toString();
559-
writeFile(host, emitterDiagnostics, sourceMapFilePath, sourceMap, /*writeByteOrderMark*/ false, sourceFiles);
600+
sourceMap = sourceMapGenerator.toString();
601+
if (!forceDtsEmit) writeFile(host, emitterDiagnostics, sourceMapFilePath, sourceMap, /*writeByteOrderMark*/ false, sourceFiles);
560602
}
561603
}
562604
else {
563605
writer.writeLine();
564606
}
565607

566608
// Write the output file
567-
writeFile(host, emitterDiagnostics, jsFilePath, writer.getText(), !!compilerOptions.emitBOM, sourceFiles, { sourceMapUrlPos });
609+
writeFile(
610+
host,
611+
emitterDiagnostics,
612+
jsFilePath,
613+
writer.getText(),
614+
!!compilerOptions.emitBOM,
615+
sourceFiles,
616+
{
617+
sourceMapUrlPos,
618+
exportedModulesFromDeclarationEmit: sourceFile?.exportedModulesFromDeclarationEmit,
619+
}
620+
);
621+
if (forceDtsEmit) {
622+
// Store the result
623+
const key = getCacheDtsEmitResultKey(sourceFileOrBundle, compilerOptions);
624+
host.getDtsEmitResultCache().set(key, {
625+
diagnostics: transform.diagnostics,
626+
text: writer.getText(),
627+
sourceMap,
628+
sourceMapData,
629+
sourceMapUrlPos,
630+
exportedModulesFromDeclarationEmit: sourceFile?.exportedModulesFromDeclarationEmit,
631+
});
632+
}
568633

569634
// Reset state
570635
writer.clear();
@@ -833,6 +898,7 @@ namespace ts {
833898
getSourceFileFromReference: returnUndefined,
834899
redirectTargetsMap: createMultiMap(),
835900
getFileIncludeReasons: notImplemented,
901+
getDtsEmitResultCache: () => new Map(),
836902
};
837903
emitFiles(
838904
notImplementedResolver,

src/compiler/program.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,7 @@ namespace ts {
11201120
// Key is a file name. Value is the (non-empty, or undefined) list of files that redirect to it.
11211121
let redirectTargetsMap = createMultiMap<Path, string>();
11221122
let usesUriStyleNodeCoreModules = false;
1123+
let cacheDtsEmitResult: ESMap<Path, CacheDtsEmitResult> | undefined;
11231124

11241125
/**
11251126
* map with
@@ -1943,6 +1944,7 @@ namespace ts {
19431944
getSourceFileFromReference: (file, ref) => program.getSourceFileFromReference(file, ref),
19441945
redirectTargetsMap,
19451946
getFileIncludeReasons: program.getFileIncludeReasons,
1947+
getDtsEmitResultCache: () => cacheDtsEmitResult ||= new Map(),
19461948
};
19471949
}
19481950

@@ -2484,8 +2486,28 @@ namespace ts {
24842486
const resolver = getTypeChecker().getEmitResolver(sourceFile, cancellationToken);
24852487
// Don't actually write any files since we're just getting diagnostics.
24862488
const emitHost = getEmitHost(noop);
2487-
const result = transformNodes(resolver, emitHost, factory, options, sourceFile ? [sourceFile] : filter(emitHost.getSourceFiles(), isSourceFileNotJson), [transformDeclarations], /*allowDtsFiles*/ false);
2488-
return result.diagnostics || emptyArray;
2489+
if (sourceFile) {
2490+
if (!outFile(options)) {
2491+
// Check from cache
2492+
const existing = cacheDtsEmitResult?.get(sourceFile.resolvedPath);
2493+
if (existing) return existing.diagnostics || emptyArray;
2494+
}
2495+
const result = transformNodes(resolver, emitHost, factory, options, [sourceFile], [transformDeclarations], /*allowDtsFiles*/ false);
2496+
if (!outFile(options)) {
2497+
(cacheDtsEmitResult ||= new Map()).set(sourceFile.resolvedPath, result);
2498+
}
2499+
return result.diagnostics || emptyArray;
2500+
}
2501+
let diagnostics: DiagnosticWithLocation[] | undefined;
2502+
forEachEmittedFile(emitHost, (_emitFileNames, sourceFileOrBundle) => {
2503+
const key = getCacheDtsEmitResultKey(sourceFileOrBundle!, options);
2504+
const existing = cacheDtsEmitResult?.get(key);
2505+
if (existing) return existing.diagnostics || emptyArray;
2506+
const result = transformNodes(resolver, emitHost, factory, options, [sourceFileOrBundle!], [transformDeclarations], /*allowDtsFiles*/ false);
2507+
diagnostics = addRange(diagnostics, result.diagnostics);
2508+
(cacheDtsEmitResult ||= new Map()).set(key, result);
2509+
});
2510+
return diagnostics || emptyArray;
24892511
});
24902512
}
24912513

0 commit comments

Comments
 (0)