"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 load_config_1 = require("@sveltejs/load-config");
const utils_1 = require("../../utils");
const importPackage_1 = require("../../importPackage");
const fdir_1 = require("fdir");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const fileCollection_1 = require("./fileCollection");
const typescript_1 = __importDefault(require("typescript"));
const DEFAULT_OPTIONS = {
    dev: true
};
const NO_GENERATE = {
    generate: false
};
const SVELTE_CONFIG_EXTENSIONS = ['js', 'cjs', 'mjs'];
const SVELTE_CONFIG_TS_EXTENSIONS = ['ts', 'mts'];
const VITE_CONFIG_EXTENSIONS = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'];
const configRegex = /\/(svelte\.config\.(js|ts|cjs|mjs|mts)|vite\.config\.(js|mjs|ts|cjs|mts|cts))$/;
const configRegexWithoutTs = /\/(svelte\.config\.(js|cjs|mjs)|vite\.config\.(js|mjs|ts|cjs|mts|cts))$/;
/**
 * Loads vite.config.* and svelte.config.{js,ts,cjs,mjs,mts} 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, processFeatures, loadFromDirectory) {
        this.globSync = globSync;
        this.fs = fs;
        this.path = path;
        this.loadFromDirectory = loadFromDirectory;
        this.configFiles = new fileCollection_1.FileMap();
        this.configFilesAsync = new fileCollection_1.FileMap();
        this.filePathToConfigPath = new fileCollection_1.FileMap();
        this.disabled = false;
        this.loadSvelteConfigTs =
            processFeatures && 'typescript' in processFeatures && !!processFeatures.typescript;
    }
    /**
     * Enable/disable loading of configs (for security reasons for example)
     */
    setDisabled(disabled) {
        this.disabled = disabled;
    }
    /**
     * Use a specific config file path instead of searching for standard config filenames.
     * Only applies within `rootDirectory` (the initial tsconfig/workspace being checked).
     */
    setExplicitConfigScope(scope) {
        this.explicitConfigScope = scope;
    }
    isInExplicitConfigScope(fileOrDirPath) {
        if (!this.explicitConfigScope) {
            return false;
        }
        const normalized = (0, utils_1.normalizePath)(fileOrDirPath);
        const root = (0, utils_1.normalizePath)(this.explicitConfigScope.rootDirectory);
        return normalized === root || normalized.startsWith(root + '/');
    }
    /**
     * 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) {
        // TODO at some point we gotta find a good way to not do this anymore. Nowadays most if not all projects have one svelte.config/vite.config file at the root of the project,
        // no need to traverse each time when there's a tsconfig.json. Something like "if Svelte 5 && tsconfig.json found then don't?"
        const targetRegex = this.loadSvelteConfigTs ? configRegex : configRegexWithoutTs;
        logger_1.Logger.log('Trying to load configs for', directory);
        try {
            if (this.explicitConfigScope && this.isInExplicitConfigScope(directory)) {
                await this.loadAndCacheConfig(this.explicitConfigScope.configPath, directory);
                return;
            }
            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 && targetRegex.test(path);
            })
                .withRelativePaths()
                .crawl(directory)
                .sync();
            const someConfigIsImmediateFileInDirectory = pathResults.length > 0 &&
                pathResults.some((res) => {
                    const dirname = this.path.dirname(res);
                    return !dirname || dirname === '.';
                });
            if (!someConfigIsImmediateFileInDirectory) {
                const configPathUpwards = this.searchConfigPathUpwards(directory);
                if (configPathUpwards) {
                    pathResults.push(this.path.relative(directory, configPathUpwards));
                }
            }
            if (pathResults.length === 0) {
                await 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);
        }
    }
    async addFallbackConfig(directory) {
        const configPath = this.searchConfigPathUpwards(directory);
        if (configPath) {
            const loadedConfigPath = await this.loadAndCacheConfig(configPath, directory);
            const config = loadedConfigPath && this.configFiles.get(loadedConfigPath);
            if (config && !config.loadConfigError && !config.isFallbackConfig) {
                return;
            }
        }
        const fallback = this.useFallbackPreprocessor(directory, false, configPath && isViteConfigPath(configPath) ? 'vite-error' : 'none');
        const path = this.path.join(directory, 'svelte.config.js');
        this.configFilesAsync.set(path, Promise.resolve(fallback));
        this.configFiles.set(path, fallback);
    }
    searchConfigPathUpwards(path) {
        if (this.explicitConfigScope && this.isInExplicitConfigScope(path)) {
            return this.explicitConfigScope.configPath;
        }
        let currentDir = path;
        let nextDir = this.path.dirname(path);
        while (currentDir !== nextDir) {
            const configPath = findViteConfigInDirectory(this.fs, this.path, currentDir) ??
                findSvelteConfigInDirectory(this.fs, this.path, currentDir, this.loadSvelteConfigTs);
            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;
            return configPath;
        }
        else {
            const newConfig = this.loadConfig(configPath, directory);
            this.configFilesAsync.set(configPath, newConfig.then(({ config }) => config));
            const { config, configFilePath } = await newConfig;
            this.configFiles.set(configFilePath, config);
            if (configFilePath !== configPath) {
                this.configFiles.set(configPath, config);
            }
            return configFilePath;
        }
    }
    async loadConfig(configPath, directory) {
        const configDirectory = this.path.dirname(configPath);
        if (this.disabled) {
            return {
                config: {
                    ...this.useFallbackPreprocessor(directory, true, getConfigSource(configPath)),
                    configSource: getConfigSource(configPath),
                    compilerOptions: {
                        ...DEFAULT_OPTIONS,
                        ...NO_GENERATE
                    },
                    loadConfigError: new Error('Config loading is disabled')
                },
                configFilePath: configPath
            };
        }
        const result = await this.loadFromDirectory(this.explicitConfigScope &&
            configPath === this.explicitConfigScope.configPath &&
            this.isInExplicitConfigScope(directory)
            ? configPath
            : configDirectory, { traverse: false });
        if (result && 'config' in result) {
            const configSource = result.configSource;
            const config = {
                ...result.config,
                configSource,
                compilerOptions: {
                    ...DEFAULT_OPTIONS,
                    ...result.config.compilerOptions,
                    ...NO_GENERATE
                }
            };
            logger_1.Logger.log('Loaded config at ', result.configFilePath);
            return {
                config,
                configFilePath: result.configFilePath
            };
        }
        const configSource = result?.configSource ?? getConfigSource(configPath);
        const error = result?.error ??
            new Error(configSource === 'vite'
                ? 'No Svelte configuration found in vite config. Is @sveltejs/vite-plugin-svelte configured?'
                : 'No Svelte configuration found');
        const errorConfigPath = result?.configFilePath ?? configPath;
        logger_1.Logger.error('Error while loading config at ', errorConfigPath);
        logger_1.Logger.error(error);
        return {
            config: {
                ...this.useFallbackPreprocessor(directory, true, configSource),
                configSource,
                compilerOptions: {
                    ...DEFAULT_OPTIONS,
                    ...NO_GENERATE
                },
                loadConfigError: error
            },
            configFilePath: errorConfigPath
        };
    }
    /**
     * 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.tryGetConfigForDirectory(file, currentDir);
            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 {
            await this.addFallbackConfig(fileDirectory);
        }
        return this.getConfig(file);
    }
    tryGetConfigForDirectory(file, fromDirectory) {
        if (this.explicitConfigScope && this.isInExplicitConfigScope(file)) {
            const config = this.configFiles.get(this.explicitConfigScope.configPath);
            if (config) {
                this.filePathToConfigPath.set(file, this.explicitConfigScope.configPath);
                return config;
            }
        }
        for (const ending of VITE_CONFIG_EXTENSIONS) {
            const configPath = this.path.join(fromDirectory, `vite.config.${ending}`);
            const config = this.configFiles.get(configPath);
            if (config) {
                this.filePathToConfigPath.set(file, configPath);
                return config;
            }
        }
        for (const ending of getSvelteConfigExtensions(this.loadSvelteConfigTs)) {
            const configPath = this.path.join(fromDirectory, `svelte.config.${ending}`);
            const config = this.configFiles.get(configPath);
            if (config) {
                this.filePathToConfigPath.set(file, configPath);
                return config;
            }
        }
    }
    useFallbackPreprocessor(path, foundConfig, configKind) {
        try {
            const sveltePreprocess = (0, importPackage_1.importSveltePreprocess)(path);
            logger_1.Logger.log(getFallbackLogMessage(foundConfig, configKind) +
                '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;
function getSvelteConfigExtensions(loadSvelteConfigTs) {
    return loadSvelteConfigTs
        ? [...SVELTE_CONFIG_EXTENSIONS, ...SVELTE_CONFIG_TS_EXTENSIONS]
        : SVELTE_CONFIG_EXTENSIONS;
}
function findSvelteConfigInDirectory(fs, pathUtils, directory, loadSvelteConfigTs) {
    for (const ending of getSvelteConfigExtensions(loadSvelteConfigTs)) {
        const configPath = pathUtils.join(directory, `svelte.config.${ending}`);
        if (fs.existsSync(configPath)) {
            return configPath;
        }
    }
}
function findViteConfigInDirectory(fs, pathUtils, directory) {
    for (const ending of VITE_CONFIG_EXTENSIONS) {
        const configPath = pathUtils.join(directory, `vite.config.${ending}`);
        if (fs.existsSync(configPath)) {
            return configPath;
        }
    }
}
function isViteConfigPath(configPath) {
    return /[/\\]vite\.config\.(js|mjs|ts|cjs|mts|cts)$/.test(configPath);
}
function getConfigSource(configPath) {
    return isViteConfigPath(configPath) ? 'vite' : 'svelte';
}
function getFallbackLogMessage(foundConfig, configKind) {
    if (foundConfig && configKind === 'svelte') {
        return 'Found svelte.config.js but there was an error loading it. ';
    }
    if (foundConfig && configKind === 'vite') {
        return 'Found vite.config but there was an error loading it. ';
    }
    if (configKind === 'vite-error') {
        return 'Found vite.config but there was an error loading Svelte options from it. ';
    }
    return 'No svelte.config.js or vite.config found. ';
}
exports.configLoader = new ConfigLoader(fdir_1.fdir, fs_1.default, path_1.default, process.features, load_config_1.loadConfig);
//# sourceMappingURL=configLoader.js.map