Skip to content

Commit

Permalink
Merge pull request #303 from timocov/perf-improves
Browse files Browse the repository at this point in the history
Performance improvements
  • Loading branch information
timocov committed Mar 9, 2024
2 parents 3036f90 + 5df139b commit c092c03
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 26 deletions.
3 changes: 3 additions & 0 deletions src/bin/dts-bundle-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ function main(): void {
warnLog('Compiler option "skipLibCheck" is disabled to properly check generated output');
}

// we want to turn this option on because in this case the compile will generate declaration diagnostics out of the box
compilerOptions.declaration = true;

let checkFailed = false;
for (const outputFile of outFilesToCheck) {
const program = ts.createProgram([outputFile], compilerOptions);
Expand Down
4 changes: 1 addition & 3 deletions src/bundle-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,6 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
return !program.isSourceFileDefaultLibrary(file);
});

verboseLog(`Input source files:\n ${sourceFiles.map((file: ts.SourceFile) => file.fileName).join('\n ')}`);

const typesUsageEvaluator = new TypesUsageEvaluator(sourceFiles, typeChecker);

return entries.map((entryConfig: EntryPointConfig) => {
Expand Down Expand Up @@ -1118,7 +1116,7 @@ export function generateDtsBundle(entries: readonly EntryPointConfig[], options:
}

for (const sourceFile of sourceFiles) {
verboseLog(`\n\n======= Preparing file: ${sourceFile.fileName} =======`);
verboseLog(`======= Processing ${sourceFile.fileName} =======`);

const updateFn = sourceFile === rootSourceFile ? updateResultForRootModule : updateResultForAnyModule;
const currentModule = getFileModuleInfo(sourceFile.fileName, criteria);
Expand Down
51 changes: 38 additions & 13 deletions src/compile-dts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as ts from 'typescript';
import { verboseLog, warnLog } from './logger';

import { getCompilerOptions } from './get-compiler-options';
import { getAbsolutePath } from './helpers/get-absolute-path';
import { checkProgramDiagnosticsErrors, checkDiagnosticsErrors } from './helpers/check-diagnostics-errors';

export interface CompileDtsResult {
Expand Down Expand Up @@ -41,24 +40,29 @@ export function compileDts(rootFiles: readonly string[], preferredConfigPath?: s
compilerOptions.tsBuildInfoFile = undefined;
compilerOptions.declarationDir = undefined;

// we want to turn this option on because in this case the compile will generate declaration diagnostics out of the box
compilerOptions.declaration = true;

if (compilerOptions.composite) {
warnLog(`Composite projects aren't supported at the time. Prefer to use non-composite project to generate declarations instead or just ignore this message if everything works fine. See https://github.com/timocov/dts-bundle-generator/issues/93`);
compilerOptions.composite = undefined;
}

const dtsFiles = getDeclarationFiles(rootFiles, compilerOptions);

verboseLog(`dts cache:\n ${Object.keys(dtsFiles).join('\n ')}\n`);
const host = createCachingCompilerHost(compilerOptions);

const host = ts.createCompilerHost(compilerOptions);
const dtsFiles = getDeclarationFiles(rootFiles, compilerOptions, host);

if (!followSymlinks) {
// note that this shouldn't affect the previous call as there we actually want to use actual path in order to compile files
// and avoid issues like "you have .ts files in node_modules"
host.realpath = (p: string) => p;
}

const moduleResolutionCache = ts.createModuleResolutionCache(host.getCurrentDirectory(), host.getCanonicalFileName, compilerOptions);

host.resolveModuleNameLiterals = (moduleLiterals: readonly ts.StringLiteralLike[], containingFile: string): ts.ResolvedModuleWithFailedLookupLocations[] => {
return moduleLiterals.map((moduleLiteral: ts.StringLiteralLike): ts.ResolvedModuleWithFailedLookupLocations => {
const resolvedModule = ts.resolveModuleName(moduleLiteral.text, containingFile, compilerOptions, host).resolvedModule;
const resolvedModule = ts.resolveModuleName(moduleLiteral.text, containingFile, compilerOptions, host, moduleResolutionCache).resolvedModule;
if (resolvedModule && !resolvedModule.isExternalLibraryImport) {
const newExt = declarationExtsRemapping[resolvedModule.extension];

Expand All @@ -77,14 +81,11 @@ export function compileDts(rootFiles: readonly string[], preferredConfigPath?: s

const originalGetSourceFile = host.getSourceFile;
host.getSourceFile = (fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) => {
const absolutePath = getAbsolutePath(fileName);
const storedValue = dtsFiles.get(absolutePath);
const storedValue = dtsFiles.get(host.getCanonicalFileName(fileName));
if (storedValue !== undefined) {
verboseLog(`dts cache match: ${absolutePath}`);
return ts.createSourceFile(fileName, storedValue, languageVersion);
}

verboseLog(`dts cache mismatch: ${absolutePath} (${fileName})`);
return originalGetSourceFile(fileName, languageVersion, onError);
};

Expand All @@ -102,6 +103,26 @@ export function compileDts(rootFiles: readonly string[], preferredConfigPath?: s
return { program, rootFilesRemapping };
}

function createCachingCompilerHost(compilerOptions: ts.CompilerOptions): ts.CompilerHost {
const host = ts.createIncrementalCompilerHost(compilerOptions);

const sourceFilesCache = new Map<string, ts.SourceFile | undefined>();

const originalGetSourceFile = host.getSourceFile;
host.getSourceFile = (fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void): ts.SourceFile | undefined => {
const key = host.getCanonicalFileName(fileName);
let cacheValue = sourceFilesCache.get(key);
if (cacheValue === undefined) {
cacheValue = originalGetSourceFile(fileName, languageVersion, onError);
sourceFilesCache.set(key, cacheValue);
}

return cacheValue;
};

return host;
}

function changeExtensionToDts(fileName: string): string {
let ext: ts.Extension | undefined;

Expand Down Expand Up @@ -130,7 +151,7 @@ function changeExtensionToDts(fileName: string): string {
/**
* @description Compiles source files into d.ts files and returns map of absolute path to file content
*/
function getDeclarationFiles(rootFiles: readonly string[], compilerOptions: ts.CompilerOptions): Map<string, string> {
function getDeclarationFiles(rootFiles: readonly string[], compilerOptions: ts.CompilerOptions, host: ts.CompilerHost): Map<string, string> {
// we must pass `declaration: true` and `noEmit: false` if we want to generate declaration files
// see https://github.com/microsoft/TypeScript/issues/24002#issuecomment-550549393
// also, we don't want to generate anything apart from declarations so that's why `emitDeclarationOnly: true` is here
Expand All @@ -143,7 +164,11 @@ function getDeclarationFiles(rootFiles: readonly string[], compilerOptions: ts.C
emitDeclarationOnly: true,
};

const program = ts.createProgram(rootFiles, compilerOptions);
// theoretically this could be dangerous because the compiler host is created with compiler options
// so technically `compilerOptions` and ones that were used to create the host might be different (and most likely will be)
// but apparently a compiler host doesn't use compiler options that much, just a few encoding/newLine oriented
// so hopefully it should be fine
const program = ts.createProgram(rootFiles, compilerOptions, host);
const allFilesAreDeclarations = program.getSourceFiles().every((s: ts.SourceFile) => s.isDeclarationFile);

const declarations = new Map<string, string>();
Expand All @@ -158,7 +183,7 @@ function getDeclarationFiles(rootFiles: readonly string[], compilerOptions: ts.C

const emitResult = program.emit(
undefined,
(fileName: string, data: string) => declarations.set(getAbsolutePath(fileName), data),
(fileName: string, data: string) => declarations.set(host.getCanonicalFileName(fileName), data),
undefined,
true
);
Expand Down
5 changes: 4 additions & 1 deletion src/helpers/check-diagnostics-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ const formatDiagnosticsHost: ts.FormatDiagnosticsHost = {
};

export function checkProgramDiagnosticsErrors(program: ts.Program): void {
if (!program.getCompilerOptions().declaration) {
throw new Error(`Something went wrong - the program doesn't have declaration option enabled`);
}

checkDiagnosticsErrors(ts.getPreEmitDiagnostics(program), 'Compiled with errors');
checkDiagnosticsErrors(program.getDeclarationDiagnostics(), 'Compiled with errors');
}

export function checkDiagnosticsErrors(diagnostics: readonly ts.Diagnostic[], failMessage: string): void {
Expand Down
36 changes: 27 additions & 9 deletions src/types-usage-evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export class TypesUsageEvaluator {
private readonly typeChecker: ts.TypeChecker;
private readonly nodesParentsMap: Map<ts.Symbol, Set<ts.Symbol>> = new Map();

private readonly usageResultCache: Map<ts.Symbol, Map<ts.Symbol, boolean>> = new Map();

public constructor(files: ts.SourceFile[], typeChecker: ts.TypeChecker) {
this.typeChecker = typeChecker;
this.computeUsages(files);
Expand All @@ -31,7 +33,12 @@ export class TypesUsageEvaluator {

private isSymbolUsedBySymbolImpl(fromSymbol: ts.Symbol, toSymbol: ts.Symbol, visitedSymbols: Set<ts.Symbol>): boolean {
if (fromSymbol === toSymbol) {
return true;
return this.setUsageCacheValue(fromSymbol, toSymbol, true);
}

const cacheResult = this.usageResultCache.get(fromSymbol)?.get(toSymbol);
if (cacheResult !== undefined) {
return cacheResult;
}

const reachableNodes = this.nodesParentsMap.get(fromSymbol);
Expand All @@ -50,7 +57,19 @@ export class TypesUsageEvaluator {

visitedSymbols.add(fromSymbol);

return false;
return this.setUsageCacheValue(fromSymbol, toSymbol, false);
}

private setUsageCacheValue(fromSymbol: ts.Symbol, toSymbol: ts.Symbol, value: boolean): boolean {
let fromSymbolCacheMap = this.usageResultCache.get(fromSymbol);
if (fromSymbolCacheMap === undefined) {
fromSymbolCacheMap = new Map();
this.usageResultCache.set(fromSymbol, fromSymbolCacheMap);
}

fromSymbolCacheMap.set(toSymbol, value);

return value;
}

private computeUsages(files: ts.SourceFile[]): void {
Expand Down Expand Up @@ -187,29 +206,28 @@ export class TypesUsageEvaluator {
}

private computeUsagesRecursively(parent: ts.Node, parentSymbol: ts.Symbol): void {
const queue = parent.getChildren();
for (const child of queue) {
ts.forEachChild(parent, (child: ts.Node) => {
if (child.kind === ts.SyntaxKind.JSDoc) {
continue;
return;
}

queue.push(...child.getChildren());
this.computeUsagesRecursively(child, parentSymbol);

if (ts.isIdentifier(child) || child.kind === ts.SyntaxKind.DefaultKeyword) {
// identifiers in labelled tuples don't have symbols for their labels
// so let's just skip them from collecting
if (ts.isNamedTupleMember(child.parent) && child.parent.name === child) {
continue;
return;
}

// `{ propertyName: name }` - in this case we don't need to handle `propertyName` as it has no symbol
if (ts.isBindingElement(child.parent) && child.parent.propertyName === child) {
continue;
return;
}

this.addUsages(this.getSymbol(child), parentSymbol);
}
}
});
}

private addUsages(childSymbol: ts.Symbol, parentSymbol: ts.Symbol): void {
Expand Down

0 comments on commit c092c03

Please sign in to comment.