svelte-language-server
Version:
A language server for Svelte
385 lines • 16.8 kB
JavaScript
"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