"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.configLoader = exports.ConfigLoader = void 0;
const logger_1 = require("../../logger");
const importPackage_1 = require("../../importPackage");
const fdir_1 = require("fdir");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const url_1 = require("url");
const fileCollection_1 = require("./fileCollection");
const typescript_1 = __importDefault(require("typescript"));
const DEFAULT_OPTIONS = {
    dev: true
};
const NO_GENERATE = {
    generate: false
};
/**
 * This function encapsulates the import call in a way
 * that TypeScript does not transpile `import()`.
 * https://github.com/microsoft/TypeScript/issues/43329
 */
const _dynamicImport = new Function('modulePath', 'return import(modulePath)');
const configRegex = /\/svelte\.config\.(js|cjs|mjs)$/;
/**
 * Loads svelte.config.{js,cjs,mjs} files. Provides both a synchronous and asynchronous
 * interface to get a config file because snapshots need access to it synchronously.
 * This means that another instance (the ts service host on startup) should make
 * sure that all config files are loaded before snapshots are retrieved.
 * Asynchronousity is needed because we use the dynamic `import()` statement.
 */
class ConfigLoader {
    constructor(globSync, fs, path, dynamicImport) {
        this.globSync = globSync;
        this.fs = fs;
        this.path = path;
        this.dynamicImport = dynamicImport;
        this.configFiles = new fileCollection_1.FileMap();
        this.configFilesAsync = new fileCollection_1.FileMap();
        this.filePathToConfigPath = new fileCollection_1.FileMap();
        this.disabled = false;
    }
    /**
     * Enable/disable loading of configs (for security reasons for example)
     */
    setDisabled(disabled) {
        this.disabled = disabled;
    }
    /**
     * Tries to load all `svelte.config.js` files below given directory
     * and the first one found inside/above that directory.
     *
     * @param directory Directory where to load the configs from
     */
    async loadConfigs(directory) {
        logger_1.Logger.log('Trying to load configs for', directory);
        try {
            const pathResults = new this.globSync({})
                .withPathSeparator('/')
                .exclude((_, path) => {
                // no / at the start, path could start with node_modules
                return path.includes('node_modules/') || path.includes('/.') || path[0] === '.';
            })
                .filter((path, isDir) => {
                return !isDir && configRegex.test(path);
            })
                .withRelativePaths()
                .crawl(directory)
                .sync();
            const someConfigIsImmediateFileInDirectory = pathResults.length > 0 && pathResults.some((res) => !this.path.dirname(res));
            if (!someConfigIsImmediateFileInDirectory) {
                const configPathUpwards = this.searchConfigPathUpwards(directory);
                if (configPathUpwards) {
                    pathResults.push(this.path.relative(directory, configPathUpwards));
                }
            }
            if (pathResults.length === 0) {
                this.addFallbackConfig(directory);
                return;
            }
            const promises = pathResults
                .map((pathResult) => this.path.join(directory, pathResult))
                .filter((pathResult) => {
                const config = this.configFiles.get(pathResult);
                return !config || config.loadConfigError;
            })
                .map(async (pathResult) => {
                await this.loadAndCacheConfig(pathResult, directory);
            });
            await Promise.all(promises);
        }
        catch (e) {
            logger_1.Logger.error(e);
        }
    }
    addFallbackConfig(directory) {
        const fallback = this.useFallbackPreprocessor(directory, false);
        const path = this.path.join(directory, 'svelte.config.js');
        this.configFilesAsync.set(path, Promise.resolve(fallback));
        this.configFiles.set(path, fallback);
    }
    searchConfigPathUpwards(path) {
        let currentDir = path;
        let nextDir = this.path.dirname(path);
        while (currentDir !== nextDir) {
            const tryFindConfigPath = (ending) => {
                const path = this.path.join(currentDir, `svelte.config.${ending}`);
                return this.fs.existsSync(path) ? path : undefined;
            };
            const configPath = tryFindConfigPath('js') || tryFindConfigPath('cjs') || tryFindConfigPath('mjs');
            if (configPath) {
                return configPath;
            }
            currentDir = nextDir;
            nextDir = this.path.dirname(currentDir);
        }
    }
    async loadAndCacheConfig(configPath, directory) {
        const loadingConfig = this.configFilesAsync.get(configPath);
        if (loadingConfig) {
            await loadingConfig;
        }
        else {
            const newConfig = this.loadConfig(configPath, directory);
            this.configFilesAsync.set(configPath, newConfig);
            this.configFiles.set(configPath, await newConfig);
        }
    }
    async loadConfig(configPath, directory) {
        try {
            let config = this.disabled
                ? {}
                : (await this.dynamicImport((0, url_1.pathToFileURL)(configPath)))?.default;
            if (!config) {
                throw new Error('Missing exports in the config. Make sure to include "export default config" or "module.exports = config"');
            }
            config = {
                ...config,
                compilerOptions: {
                    ...DEFAULT_OPTIONS,
                    ...config.compilerOptions,
                    ...NO_GENERATE
                }
            };
            logger_1.Logger.log('Loaded config at ', configPath);
            return config;
        }
        catch (err) {
            logger_1.Logger.error('Error while loading config at ', configPath);
            logger_1.Logger.error(err);
            const config = {
                ...this.useFallbackPreprocessor(directory, true),
                compilerOptions: {
                    ...DEFAULT_OPTIONS,
                    ...NO_GENERATE
                },
                loadConfigError: err
            };
            return config;
        }
    }
    /**
     * Returns config associated to file. If no config is found, the file
     * was called in a context where no config file search was done before,
     * which can happen
     * - if TS intellisense is turned off and the search did not run on tsconfig init
     * - if the file was opened not through the TS service crawl, but through the LSP
     *
     * @param file
     */
    getConfig(file) {
        const cached = this.filePathToConfigPath.get(file);
        if (cached) {
            return this.configFiles.get(cached);
        }
        let currentDir = file;
        let nextDir = this.path.dirname(file);
        while (currentDir !== nextDir) {
            currentDir = nextDir;
            const config = this.tryGetConfig(file, currentDir, 'js') ||
                this.tryGetConfig(file, currentDir, 'cjs') ||
                this.tryGetConfig(file, currentDir, 'mjs');
            if (config) {
                return config;
            }
            nextDir = this.path.dirname(currentDir);
        }
    }
    /**
     * Like `getConfig`, but will search for a config above if no config found.
     */
    async awaitConfig(file) {
        const config = this.getConfig(file);
        if (config) {
            return config;
        }
        const fileDirectory = this.path.dirname(file);
        const configPath = this.searchConfigPathUpwards(fileDirectory);
        if (configPath) {
            await this.loadAndCacheConfig(configPath, fileDirectory);
        }
        else {
            this.addFallbackConfig(fileDirectory);
        }
        return this.getConfig(file);
    }
    tryGetConfig(file, fromDirectory, configFileEnding) {
        const path = this.path.join(fromDirectory, `svelte.config.${configFileEnding}`);
        const config = this.configFiles.get(path);
        if (config) {
            this.filePathToConfigPath.set(file, path);
            return config;
        }
    }
    useFallbackPreprocessor(path, foundConfig) {
        try {
            const sveltePreprocess = (0, importPackage_1.importSveltePreprocess)(path);
            logger_1.Logger.log((foundConfig
                ? 'Found svelte.config.js but there was an error loading it. '
                : 'No svelte.config.js found. ') +
                'Using https://github.com/sveltejs/svelte-preprocess as fallback');
            return {
                preprocess: sveltePreprocess({
                    // 4.x does not have transpileOnly anymore, but if the user has version 3.x
                    // in his repo, that one is loaded instead, for which we still need this.
                    typescript: {
                        transpileOnly: true,
                        compilerOptions: { sourceMap: true, inlineSourceMap: false }
                    }
                }),
                isFallbackConfig: true
            };
        }
        catch (e) {
            // User doesn't have svelte-preprocess installed, provide a barebones TS preprocessor
            return {
                preprocess: {
                    // @ts-ignore name property exists in Svelte 4 onwards
                    name: 'svelte-language-tools-ts-fallback-preprocessor',
                    script: ({ content, attributes, filename }) => {
                        if (attributes.lang !== 'ts')
                            return;
                        const { outputText, sourceMapText } = typescript_1.default.transpileModule(content, {
                            fileName: filename,
                            compilerOptions: {
                                module: typescript_1.default.ModuleKind.ESNext,
                                target: typescript_1.default.ScriptTarget.ESNext,
                                sourceMap: true,
                                verbatimModuleSyntax: true
                            }
                        });
                        return { code: outputText, map: sourceMapText };
                    }
                },
                isFallbackConfig: true
            };
        }
    }
}
exports.ConfigLoader = ConfigLoader;
exports.configLoader = new ConfigLoader(fdir_1.fdir, fs_1.default, path_1.default, _dynamicImport);
//# sourceMappingURL=configLoader.js.map