svelte-language-server
Version:
A language server for Svelte
265 lines • 10.9 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 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