@fimbul/wotan
Version: 
Pluggable TypeScript and JavaScript linter
248 lines • 10.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProjectHost = void 0;
const tslib_1 = require("tslib");
const ts = require("typescript");
const utils_1 = require("./utils");
const path = require("path");
const bind_decorator_1 = require("bind-decorator");
const debug = require("debug");
const log = debug('wotan:projectHost');
const additionalExtensions = ['.json'];
// @internal
class ProjectHost {
    constructor(cwd, config, fs, configManager, processorLoader) {
        this.cwd = cwd;
        this.config = config;
        this.fs = fs;
        this.configManager = configManager;
        this.processorLoader = processorLoader;
        this.reverseMap = new Map();
        this.files = [];
        this.directoryEntries = new Map();
        this.processedFiles = new Map();
        this.sourceFileCache = new Map();
        this.fileContent = new Map();
        this.tsconfigCache = new Map();
        this.commandLineCache = new Map();
        this.parseConfigHost = {
            useCaseSensitiveFileNames: this.useCaseSensitiveFileNames(),
            readDirectory: (rootDir, extensions, excludes, includes, depth) => this.readDirectory(rootDir, extensions, excludes, includes, depth),
            fileExists: (f) => this.fileExists(f),
            readFile: (f) => this.readFile(f),
        };
        this.getCanonicalFileName = ts.sys.useCaseSensitiveFileNames ? (f) => f : (f) => f.toLowerCase();
        this.moduleResolutionCache = ts.createModuleResolutionCache(this.cwd, this.getCanonicalFileName);
        this.compilerOptions = {};
        this.getDefaultLibFileName = ts.getDefaultLibFilePath;
        this.realpath = this.fs.realpath === undefined ? undefined : (fileName) => this.fs.realpath(fileName);
    }
    getProcessedFileInfo(fileName) {
        return this.processedFiles.get(fileName);
    }
    readDirectory(rootDir, extensions, excludes, includes, depth) {
        return ts.matchFiles(rootDir, extensions, excludes, includes, this.useCaseSensitiveFileNames(), this.cwd, depth, (dir) => utils_1.resolveCachedResult(this.directoryEntries, dir, this.processDirectory), (f) => this.safeRealpath(f));
    }
    /**
     * Try to find and load the configuration for a file.
     * If it fails, just continue as if there was no config.
     * This may happen during project setup if there is an invalid config file anywhere in a scanned folder.
     */
    tryFindConfig(file) {
        try {
            return this.configManager.find(file);
        }
        catch (e) {
            log("Error while loading configuration for '%s': %s", file, e.message);
            return;
        }
    }
    processDirectory(dir) {
        const files = [];
        const directories = [];
        const result = { files, directories };
        let entries;
        try {
            entries = this.fs.readDirectory(dir);
        }
        catch {
            return result;
        }
        for (const entry of entries) {
            switch (entry.kind) {
                case 1 /* File */: {
                    const fileName = `${dir}/${entry.name}`;
                    if (!utils_1.hasSupportedExtension(fileName, additionalExtensions)) {
                        const c = this.config || this.tryFindConfig(fileName);
                        const processor = c && this.configManager.getProcessor(c, fileName);
                        if (processor) {
                            const ctor = this.processorLoader.loadProcessor(processor);
                            const suffix = ctor.getSuffixForFile({
                                fileName,
                                getSettings: () => this.configManager.getSettings(c, fileName),
                                readFile: () => this.fs.readFile(fileName),
                            });
                            const newName = fileName + suffix;
                            if (utils_1.hasSupportedExtension(newName, additionalExtensions)) {
                                files.push(entry.name + suffix);
                                this.reverseMap.set(newName, fileName);
                                break;
                            }
                        }
                    }
                    files.push(entry.name);
                    this.files.push(fileName);
                    break;
                }
                case 2 /* Directory */:
                    directories.push(entry.name);
            }
        }
        return result;
    }
    fileExists(file) {
        switch (this.fs.getKind(file)) {
            case 2 /* Directory */:
            case 3 /* Other */:
                return false;
            case 1 /* File */:
                return true;
            default:
                return utils_1.hasSupportedExtension(file, additionalExtensions) && this.getFileSystemFile(file) !== undefined;
        }
    }
    directoryExists(dir) {
        return this.fs.isDirectory(dir);
    }
    getFileSystemFile(file) {
        if (this.files.includes(file))
            return file;
        const reverse = this.reverseMap.get(file);
        if (reverse !== undefined)
            return reverse;
        const dirname = path.posix.dirname(file);
        if (this.directoryEntries.has(dirname))
            return;
        if (this.fs.isFile(file))
            return file;
        this.directoryEntries.set(dirname, this.processDirectory(dirname));
        return this.getFileSystemFile(file);
    }
    readFile(file) {
        return utils_1.resolveCachedResult(this.fileContent, file, (f) => this.fs.readFile(f));
    }
    readProcessedFile(file) {
        const realFile = this.getFileSystemFile(file);
        if (realFile === undefined)
            return;
        let content = this.fs.readFile(realFile);
        const config = this.config || this.tryFindConfig(realFile);
        if (config === undefined)
            return content;
        const processorPath = this.configManager.getProcessor(config, realFile);
        if (processorPath === undefined)
            return content;
        const ctor = this.processorLoader.loadProcessor(processorPath);
        const processor = new ctor({
            source: content,
            sourceFileName: realFile,
            targetFileName: file,
            settings: this.configManager.getSettings(config, realFile),
        });
        this.processedFiles.set(file, {
            processor,
            originalContent: content,
            originalName: realFile,
        });
        content = processor.preprocess();
        return content;
    }
    writeFile() { }
    useCaseSensitiveFileNames() {
        return ts.sys.useCaseSensitiveFileNames;
    }
    getNewLine() {
        return this.compilerOptions.newLine === ts.NewLineKind.CarriageReturnLineFeed ? '\r\n' : '\n';
    }
    safeRealpath(f) {
        if (this.realpath !== undefined) {
            try {
                return this.realpath(f);
            }
            catch { }
        }
        return f;
    }
    getCurrentDirectory() {
        return this.cwd;
    }
    getDirectories(dir) {
        const cached = this.directoryEntries.get(dir);
        if (cached !== undefined)
            return cached.directories.slice();
        return utils_1.mapDefined(this.fs.readDirectory(dir), (entry) => entry.kind === 2 /* Directory */ ? entry.name : undefined);
    }
    getSourceFile(fileName, languageVersion) {
        return utils_1.resolveCachedResult(this.sourceFileCache, fileName, () => {
            const content = this.readProcessedFile(fileName);
            return content !== undefined ? ts.createSourceFile(fileName, content, languageVersion, true) : undefined;
        });
    }
    createProgram(rootNames, options, oldProgram, projectReferences) {
        options = { ...options, suppressOutputPathCheck: true };
        this.compilerOptions = options;
        this.moduleResolutionCache = ts.createModuleResolutionCache(this.cwd, this.getCanonicalFileName, options);
        return ts.createProgram({ rootNames, options, oldProgram, projectReferences, host: this });
    }
    updateSourceFile(sourceFile) {
        this.sourceFileCache.set(sourceFile.fileName, sourceFile);
    }
    updateProgram(program) {
        return ts.createProgram({
            rootNames: program.getRootFileNames(),
            options: program.getCompilerOptions(),
            oldProgram: program,
            projectReferences: program.getProjectReferences(),
            host: this,
        });
    }
    onReleaseOldSourceFile(sourceFile) {
        // this is only called for paths that are no longer referenced
        // it's safe to remove the cache entry completely because it won't be called with updated SourceFiles
        this.uncacheFile(sourceFile.fileName);
    }
    uncacheFile(fileName) {
        this.sourceFileCache.delete(fileName);
        this.processedFiles.delete(fileName);
    }
    getParsedCommandLine(fileName) {
        return utils_1.resolveCachedResult(this.commandLineCache, fileName, this.parseConfigFile);
    }
    parseConfigFile(fileName) {
        // Note to future self: it's highly unlikely that a tsconfig of a project reference is used as base config for another tsconfig.
        // Therefore it doesn't make such sense to read or write the tsconfigCache here.
        const sourceFile = this.getSourceFile(fileName, ts.ScriptTarget.JSON);
        if (sourceFile === undefined)
            return;
        return ts.parseJsonSourceFileConfigFileContent(sourceFile, this.parseConfigHost, path.dirname(fileName), undefined, fileName, undefined, undefined, this.tsconfigCache);
    }
    resolveModuleNames(names, file, _, reference, options) {
        const seen = new Map();
        const resolve = (name) => ts.resolveModuleName(name, file, options, this, this.moduleResolutionCache, reference).resolvedModule;
        return names.map((name) => utils_1.resolveCachedResult(seen, name, resolve));
    }
}
tslib_1.__decorate([
    bind_decorator_1.bind,
    tslib_1.__metadata("design:type", Function),
    tslib_1.__metadata("design:paramtypes", [String]),
    tslib_1.__metadata("design:returntype", Object)
], ProjectHost.prototype, "processDirectory", null);
tslib_1.__decorate([
    bind_decorator_1.bind,
    tslib_1.__metadata("design:type", Function),
    tslib_1.__metadata("design:paramtypes", [String]),
    tslib_1.__metadata("design:returntype", void 0)
], ProjectHost.prototype, "parseConfigFile", null);
exports.ProjectHost = ProjectHost;
//# sourceMappingURL=project-host.js.map