Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance improvements #303

Merged
merged 4 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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