"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getServiceForTsconfig = exports.forAllServices = exports.getService = exports.__resetCache = void 0;
const path_1 = require("path");
const typescript_1 = __importDefault(require("typescript"));
const importPackage_1 = require("../../importPackage");
const configLoader_1 = require("../../lib/documents/configLoader");
const fileCollection_1 = require("../../lib/documents/fileCollection");
const logger_1 = require("../../logger");
const utils_1 = require("../../utils");
const DocumentSnapshot_1 = require("./DocumentSnapshot");
const module_loader_1 = require("./module-loader");
const SnapshotManager_1 = require("./SnapshotManager");
const utils_2 = require("./utils");
const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; // 20 MB
const services = new fileCollection_1.FileMap();
const serviceSizeMap = new fileCollection_1.FileMap();
const configWatchers = new fileCollection_1.FileMap();
const extendedConfigWatchers = new fileCollection_1.FileMap();
const extendedConfigToTsConfigPath = new fileCollection_1.FileMap();
const configFileModifiedTime = new fileCollection_1.FileMap();
const configFileForOpenFiles = new fileCollection_1.FileMap();
const pendingReloads = new fileCollection_1.FileSet();
/**
 * For testing only: Reset the cache for services.
 * Try to refactor this some day so that this file provides
 * a setup function which creates all this nicely instead.
 */
function __resetCache() {
    services.clear();
    serviceSizeMap.clear();
    configFileForOpenFiles.clear();
}
exports.__resetCache = __resetCache;
async function getService(path, workspaceUris, docContext) {
    const getCanonicalFileName = (0, utils_1.createGetCanonicalFileName)(docContext.tsSystem.useCaseSensitiveFileNames);
    const tsconfigPath = configFileForOpenFiles.get(path) ??
        (0, utils_2.findTsConfigPath)(path, workspaceUris, docContext.tsSystem.fileExists, getCanonicalFileName);
    if (tsconfigPath) {
        configFileForOpenFiles.set(path, tsconfigPath);
        return getServiceForTsconfig(tsconfigPath, (0, path_1.dirname)(tsconfigPath), docContext);
    }
    // Find closer boundary: workspace uri or node_modules
    const nearestWorkspaceUri = (0, utils_2.getNearestWorkspaceUri)(workspaceUris, path, getCanonicalFileName);
    const lastNodeModulesIdx = path.split('/').lastIndexOf('node_modules') + 2;
    const nearestNodeModulesBoundary = lastNodeModulesIdx === 1
        ? undefined
        : path.split('/').slice(0, lastNodeModulesIdx).join('/');
    const nearestBoundary = (nearestNodeModulesBoundary?.length ?? 0) > (nearestWorkspaceUri?.length ?? 0)
        ? nearestNodeModulesBoundary
        : nearestWorkspaceUri;
    return getServiceForTsconfig(tsconfigPath, (nearestBoundary && (0, utils_1.urlToPath)(nearestBoundary)) ??
        docContext.tsSystem.getCurrentDirectory(), docContext);
}
exports.getService = getService;
async function forAllServices(cb) {
    for (const service of services.values()) {
        cb(await service);
    }
}
exports.forAllServices = forAllServices;
/**
 * @param tsconfigPath has to be absolute
 * @param docContext
 */
async function getServiceForTsconfig(tsconfigPath, workspacePath, docContext) {
    const tsconfigPathOrWorkspacePath = tsconfigPath || workspacePath;
    const reloading = pendingReloads.has(tsconfigPath);
    let service;
    if (reloading || !services.has(tsconfigPathOrWorkspacePath)) {
        if (reloading) {
            logger_1.Logger.log('Reloading ts service at ', tsconfigPath, ' due to config updated');
        }
        else {
            logger_1.Logger.log('Initialize new ts service at ', tsconfigPath);
        }
        pendingReloads.delete(tsconfigPath);
        const newService = createLanguageService(tsconfigPath, workspacePath, docContext);
        services.set(tsconfigPathOrWorkspacePath, newService);
        service = await newService;
    }
    else {
        service = await services.get(tsconfigPathOrWorkspacePath);
    }
    return service;
}
exports.getServiceForTsconfig = getServiceForTsconfig;
async function createLanguageService(tsconfigPath, workspacePath, docContext) {
    const { tsSystem } = docContext;
    const { options: compilerOptions, errors: configErrors, fileNames: files, raw, extendedConfigPaths } = getParsedConfig();
    // raw is the tsconfig merged with extending config
    // see: https://github.com/microsoft/TypeScript/blob/08e4f369fbb2a5f0c30dee973618d65e6f7f09f8/src/compiler/commandLineParser.ts#L2537
    const snapshotManager = new SnapshotManager_1.SnapshotManager(docContext.globalSnapshotsManager, raw, workspacePath, files);
    // Load all configs within the tsconfig scope and the one above so that they are all loaded
    // by the time they need to be accessed synchronously by DocumentSnapshots.
    await configLoader_1.configLoader.loadConfigs(workspacePath);
    const tsSystemWithPackageJsonCache = {
        ...tsSystem,
        /**
         * While TypeScript doesn't cache package.json in the tsserver, they do cache the
         * information they get from it within other internal APIs. We'll somewhat do the same
         * by caching the text of the package.json file here.
         */
        readFile: (path, encoding) => {
            if ((0, path_1.basename)(path) === 'package.json') {
                return docContext.globalSnapshotsManager.getPackageJson(path)?.text;
            }
            return tsSystem.readFile(path, encoding);
        }
    };
    const svelteModuleLoader = (0, module_loader_1.createSvelteModuleLoader)(getSnapshot, compilerOptions, tsSystemWithPackageJsonCache, typescript_1.default);
    let svelteTsPath;
    try {
        // For when svelte2tsx/svelte-check is part of node_modules, for example VS Code extension
        svelteTsPath = (0, path_1.dirname)(require.resolve(docContext.ambientTypesSource));
    }
    catch (e) {
        // Fall back to dirname
        svelteTsPath = __dirname;
    }
    const sveltePackageInfo = (0, importPackage_1.getPackageInfo)('svelte', tsconfigPath || workspacePath);
    const isSvelte3 = sveltePackageInfo.version.major === 3;
    const svelteHtmlDeclaration = isSvelte3
        ? undefined
        : (0, path_1.join)(sveltePackageInfo.path, 'svelte-html.d.ts');
    const svelteHtmlFallbackIfNotExist = svelteHtmlDeclaration && tsSystem.fileExists(svelteHtmlDeclaration)
        ? svelteHtmlDeclaration
        : './svelte-jsx-v4.d.ts';
    const svelteTsxFiles = (isSvelte3
        ? ['./svelte-shims.d.ts', './svelte-jsx.d.ts', './svelte-native-jsx.d.ts']
        : ['./svelte-shims-v4.d.ts', svelteHtmlFallbackIfNotExist, './svelte-native-jsx.d.ts']).map((f) => tsSystem.resolvePath((0, path_1.resolve)(svelteTsPath, f)));
    let languageServiceReducedMode = false;
    let projectVersion = 0;
    const getCanonicalFileName = (0, utils_1.createGetCanonicalFileName)(tsSystem.useCaseSensitiveFileNames);
    const host = {
        log: (message) => logger_1.Logger.debug(`[ts] ${message}`),
        getCompilationSettings: () => compilerOptions,
        getScriptFileNames,
        getScriptVersion: (fileName) => getSnapshot(fileName).version.toString(),
        getScriptSnapshot: getSnapshot,
        getCurrentDirectory: () => workspacePath,
        getDefaultLibFileName: typescript_1.default.getDefaultLibFilePath,
        fileExists: svelteModuleLoader.fileExists,
        readFile: svelteModuleLoader.readFile,
        resolveModuleNames: svelteModuleLoader.resolveModuleNames,
        readDirectory: svelteModuleLoader.readDirectory,
        getDirectories: tsSystem.getDirectories,
        useCaseSensitiveFileNames: () => tsSystem.useCaseSensitiveFileNames,
        getScriptKind: (fileName) => getSnapshot(fileName).scriptKind,
        getProjectVersion: () => projectVersion.toString(),
        getNewLine: () => tsSystem.newLine,
        resolveTypeReferenceDirectiveReferences: svelteModuleLoader.resolveTypeReferenceDirectiveReferences
    };
    let languageService = typescript_1.default.createLanguageService(host);
    const transformationConfig = {
        transformOnTemplateError: docContext.transformOnTemplateError,
        typingsNamespace: raw?.svelteOptions?.namespace || 'svelteHTML'
    };
    const onSnapshotChange = () => {
        projectVersion++;
    };
    docContext.globalSnapshotsManager.onChange(onSnapshotChange);
    reduceLanguageServiceCapabilityIfFileSizeTooBig();
    updateExtendedConfigDependents();
    watchConfigFile();
    return {
        tsconfigPath,
        compilerOptions,
        configErrors,
        getService: () => languageService,
        updateSnapshot,
        deleteSnapshot,
        updateProjectFiles,
        updateTsOrJsFile,
        hasFile,
        fileBelongsToProject,
        snapshotManager,
        dispose
    };
    function deleteSnapshot(filePath) {
        svelteModuleLoader.deleteFromModuleCache(filePath);
        snapshotManager.delete(filePath);
        configFileForOpenFiles.delete(filePath);
    }
    function updateSnapshot(documentOrFilePath) {
        return typeof documentOrFilePath === 'string'
            ? updateSnapshotFromFilePath(documentOrFilePath)
            : updateSnapshotFromDocument(documentOrFilePath);
    }
    function updateSnapshotFromDocument(document) {
        const filePath = document.getFilePath() || '';
        const prevSnapshot = snapshotManager.get(filePath);
        if (prevSnapshot?.version === document.version) {
            return prevSnapshot;
        }
        if (!prevSnapshot) {
            svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath);
        }
        const newSnapshot = DocumentSnapshot_1.DocumentSnapshot.fromDocument(document, transformationConfig);
        snapshotManager.set(filePath, newSnapshot);
        if (prevSnapshot && prevSnapshot.scriptKind !== newSnapshot.scriptKind) {
            // Restart language service as it doesn't handle script kind changes.
            languageService.dispose();
            languageService = typescript_1.default.createLanguageService(host);
        }
        return newSnapshot;
    }
    function updateSnapshotFromFilePath(filePath) {
        const prevSnapshot = snapshotManager.get(filePath);
        if (prevSnapshot) {
            return prevSnapshot;
        }
        svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath);
        const newSnapshot = DocumentSnapshot_1.DocumentSnapshot.fromFilePath(filePath, docContext.createDocument, transformationConfig, tsSystem);
        snapshotManager.set(filePath, newSnapshot);
        return newSnapshot;
    }
    function getSnapshot(fileName) {
        fileName = (0, utils_2.ensureRealSvelteFilePath)(fileName);
        let doc = snapshotManager.get(fileName);
        if (doc) {
            return doc;
        }
        svelteModuleLoader.deleteUnresolvedResolutionsFromCache(fileName);
        doc = DocumentSnapshot_1.DocumentSnapshot.fromFilePath(fileName, docContext.createDocument, transformationConfig, tsSystem);
        snapshotManager.set(fileName, doc);
        return doc;
    }
    function updateProjectFiles() {
        projectVersion++;
        const projectFileCountBefore = snapshotManager.getProjectFileNames().length;
        snapshotManager.updateProjectFiles();
        const projectFileCountAfter = snapshotManager.getProjectFileNames().length;
        if (projectFileCountAfter <= projectFileCountBefore) {
            return;
        }
        reduceLanguageServiceCapabilityIfFileSizeTooBig();
    }
    function getScriptFileNames() {
        const projectFiles = languageServiceReducedMode
            ? []
            : snapshotManager.getProjectFileNames();
        const canonicalProjectFileNames = new Set(projectFiles.map(getCanonicalFileName));
        return Array.from(new Set([
            ...projectFiles,
            // project file is read from the file system so it's more likely to have
            // the correct casing
            ...snapshotManager
                .getFileNames()
                .filter((file) => !canonicalProjectFileNames.has(getCanonicalFileName(file))),
            ...svelteTsxFiles
        ]));
    }
    function hasFile(filePath) {
        return snapshotManager.has(filePath);
    }
    function fileBelongsToProject(filePath, isNew) {
        filePath = (0, utils_1.normalizePath)(filePath);
        return hasFile(filePath) || (isNew && getParsedConfig().fileNames.includes(filePath));
    }
    function updateTsOrJsFile(fileName, changes) {
        if (!snapshotManager.has(fileName)) {
            svelteModuleLoader.deleteUnresolvedResolutionsFromCache(fileName);
        }
        snapshotManager.updateTsOrJsFile(fileName, changes);
    }
    function getParsedConfig() {
        const forcedCompilerOptions = {
            allowNonTsExtensions: true,
            target: typescript_1.default.ScriptTarget.Latest,
            allowJs: true,
            noEmit: true,
            declaration: false,
            skipLibCheck: true
        };
        // always let ts parse config to get default compilerOption
        let configJson = (tsconfigPath && typescript_1.default.readConfigFile(tsconfigPath, tsSystem.readFile).config) ||
            getDefaultJsConfig();
        // Only default exclude when no extends for now
        if (!configJson.extends) {
            configJson = Object.assign({
                exclude: getDefaultExclude()
            }, configJson);
        }
        const extendedConfigPaths = new Set();
        const { extendedConfigCache } = docContext;
        const cacheMonitorProxy = {
            ...docContext.extendedConfigCache,
            get(key) {
                extendedConfigPaths.add(key);
                return extendedConfigCache.get(key);
            },
            has(key) {
                extendedConfigPaths.add(key);
                return extendedConfigCache.has(key);
            },
            set(key, value) {
                extendedConfigPaths.add(key);
                return extendedConfigCache.set(key, value);
            }
        };
        const parsedConfig = typescript_1.default.parseJsonConfigFileContent(configJson, tsSystem, workspacePath, forcedCompilerOptions, tsconfigPath, undefined, [
            {
                extension: 'svelte',
                isMixedContent: true,
                // Deferred was added in a later TS version, fall back to tsx
                // If Deferred exists, this means that all Svelte files are included
                // in parsedConfig.fileNames
                scriptKind: typescript_1.default.ScriptKind.Deferred ?? typescript_1.default.ScriptKind.TS
            }
        ], cacheMonitorProxy);
        const compilerOptions = {
            ...parsedConfig.options,
            ...forcedCompilerOptions
        };
        if (!compilerOptions.moduleResolution ||
            compilerOptions.moduleResolution === typescript_1.default.ModuleResolutionKind.Classic) {
            compilerOptions.moduleResolution =
                // NodeJS: up to 4.9, Node10: since 5.0
                typescript_1.default.ModuleResolutionKind.NodeJs ?? typescript_1.default.ModuleResolutionKind.Node10;
        }
        if (!compilerOptions.module ||
            [
                typescript_1.default.ModuleKind.AMD,
                typescript_1.default.ModuleKind.CommonJS,
                typescript_1.default.ModuleKind.ES2015,
                typescript_1.default.ModuleKind.None,
                typescript_1.default.ModuleKind.System,
                typescript_1.default.ModuleKind.UMD
            ].includes(compilerOptions.module)) {
            compilerOptions.module = typescript_1.default.ModuleKind.ESNext;
        }
        // detect which JSX namespace to use (svelte | svelteNative) if not specified or not compatible
        if (!compilerOptions.jsxFactory || !compilerOptions.jsxFactory.startsWith('svelte')) {
            //override if we detect svelte-native
            if (workspacePath) {
                try {
                    const svelteNativePkgInfo = (0, importPackage_1.getPackageInfo)('svelte-native', workspacePath);
                    if (svelteNativePkgInfo.path) {
                        // For backwards compatibility
                        parsedConfig.raw.svelteOptions = parsedConfig.raw.svelteOptions || {};
                        parsedConfig.raw.svelteOptions.namespace = 'svelteNative.JSX';
                    }
                }
                catch (e) {
                    //we stay regular svelte
                }
            }
        }
        return {
            ...parsedConfig,
            fileNames: parsedConfig.fileNames.map(utils_1.normalizePath),
            options: compilerOptions,
            extendedConfigPaths
        };
    }
    /**
     * This should only be used when there's no jsconfig/tsconfig at all
     */
    function getDefaultJsConfig() {
        return {
            compilerOptions: {
                maxNodeModuleJsDepth: 2,
                allowSyntheticDefaultImports: true
            },
            // Necessary to not flood the initial files
            // with potentially completely unrelated .ts/.js files:
            include: []
        };
    }
    function getDefaultExclude() {
        return ['node_modules', ...SnapshotManager_1.ignoredBuildDirectories];
    }
    /**
     * Disable usage of project files.
     * running language service in a reduced mode for
     * large projects with improperly excluded tsconfig.
     */
    function reduceLanguageServiceCapabilityIfFileSizeTooBig() {
        if (exceedsTotalSizeLimitForNonTsFiles(compilerOptions, tsconfigPath, snapshotManager, tsSystem)) {
            languageService.cleanupSemanticCache();
            languageServiceReducedMode = true;
            docContext.notifyExceedSizeLimit?.();
        }
    }
    function dispose() {
        languageService.dispose();
        snapshotManager.dispose();
        configWatchers.get(tsconfigPath)?.close();
        configWatchers.delete(tsconfigPath);
        configFileForOpenFiles.clear();
        docContext.globalSnapshotsManager.removeChangeListener(onSnapshotChange);
    }
    function updateExtendedConfigDependents() {
        extendedConfigPaths.forEach((extendedConfig) => {
            let dependedTsConfig = extendedConfigToTsConfigPath.get(extendedConfig);
            if (!dependedTsConfig) {
                dependedTsConfig = new fileCollection_1.FileSet(tsSystem.useCaseSensitiveFileNames);
                extendedConfigToTsConfigPath.set(extendedConfig, dependedTsConfig);
            }
            dependedTsConfig.add(tsconfigPath);
        });
    }
    function watchConfigFile() {
        if (!tsSystem.watchFile || !docContext.watchTsConfig) {
            return;
        }
        if (!configWatchers.has(tsconfigPath) && tsconfigPath) {
            configFileModifiedTime.set(tsconfigPath, tsSystem.getModifiedTime?.(tsconfigPath));
            configWatchers.set(tsconfigPath, 
            // for some reason setting the polling interval is necessary, else some error in TS is thrown
            tsSystem.watchFile(tsconfigPath, watchConfigCallback, 1000));
        }
        for (const config of extendedConfigPaths) {
            if (extendedConfigWatchers.has(config)) {
                continue;
            }
            configFileModifiedTime.set(config, tsSystem.getModifiedTime?.(config));
            extendedConfigWatchers.set(config, 
            // for some reason setting the polling interval is necessary, else some error in TS is thrown
            tsSystem.watchFile(config, createWatchExtendedConfigCallback(docContext), 1000));
        }
    }
    async function watchConfigCallback(fileName, kind, modifiedTime) {
        if (kind === typescript_1.default.FileWatcherEventKind.Changed &&
            !configFileModified(fileName, modifiedTime ?? tsSystem.getModifiedTime?.(fileName))) {
            return;
        }
        dispose();
        if (kind === typescript_1.default.FileWatcherEventKind.Changed) {
            scheduleReload(fileName);
        }
        else if (kind === typescript_1.default.FileWatcherEventKind.Deleted) {
            services.delete(fileName);
            configFileForOpenFiles.clear();
        }
        docContext.onProjectReloaded?.();
    }
}
/**
 * adopted from https://github.com/microsoft/TypeScript/blob/3c8e45b304b8572094c5d7fbb9cd768dbf6417c0/src/server/editorServices.ts#L1955
 */
function exceedsTotalSizeLimitForNonTsFiles(compilerOptions, tsconfigPath, snapshotManager, tsSystem) {
    if (compilerOptions.disableSizeLimit) {
        return false;
    }
    let availableSpace = maxProgramSizeForNonTsFiles;
    serviceSizeMap.set(tsconfigPath, 0);
    serviceSizeMap.forEach((size) => {
        availableSpace -= size;
    });
    let totalNonTsFileSize = 0;
    const fileNames = snapshotManager.getProjectFileNames();
    for (const fileName of fileNames) {
        if ((0, utils_2.hasTsExtensions)(fileName)) {
            continue;
        }
        totalNonTsFileSize += tsSystem.getFileSize?.(fileName) ?? 0;
        if (totalNonTsFileSize > availableSpace) {
            const top5LargestFiles = fileNames
                .filter((name) => !(0, utils_2.hasTsExtensions)(name))
                .map((name) => ({ name, size: tsSystem.getFileSize?.(name) ?? 0 }))
                .sort((a, b) => b.size - a.size)
                .slice(0, 5);
            logger_1.Logger.log(`Non TS file size exceeded limit (${totalNonTsFileSize}). ` +
                `Largest files: ${top5LargestFiles
                    .map((file) => `${file.name}:${file.size}`)
                    .join(', ')}`);
            return true;
        }
    }
    serviceSizeMap.set(tsconfigPath, totalNonTsFileSize);
    return false;
}
/**
 * shared watcher callback can't be within `createLanguageService`
 * because it would reference the closure
 * So that GC won't drop it and cause memory leaks
 */
function createWatchExtendedConfigCallback(docContext) {
    return async (fileName, kind, modifiedTime) => {
        if (kind === typescript_1.default.FileWatcherEventKind.Changed &&
            !configFileModified(fileName, modifiedTime ?? docContext.tsSystem.getModifiedTime?.(fileName))) {
            return;
        }
        docContext.extendedConfigCache.delete(fileName);
        const promises = Array.from(extendedConfigToTsConfigPath.get(fileName) ?? []).map(async (config) => {
            const oldService = services.get(config);
            scheduleReload(config);
            (await oldService)?.dispose();
        });
        await Promise.all(promises);
        docContext.onProjectReloaded?.();
    };
}
/**
 * check if file content is modified instead of attributes changed
 */
function configFileModified(fileName, modifiedTime) {
    const previousModifiedTime = configFileModifiedTime.get(fileName);
    if (!modifiedTime || !previousModifiedTime) {
        return true;
    }
    if (previousModifiedTime >= modifiedTime) {
        return false;
    }
    configFileModifiedTime.set(fileName, modifiedTime);
    return true;
}
/**
 * schedule to the service reload to the next time the
 * service in requested
 * if there's still files opened it should be restarted
 * in the onProjectReloaded hooks
 */
function scheduleReload(fileName) {
    // don't delete service from map yet as it could result in a race condition
    // where a file update is received before the service is reloaded, swallowing the update
    pendingReloads.add(fileName);
}
//# sourceMappingURL=service.js.map