diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 78056e390..9be1ca55e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,6 +8,8 @@ jobs: steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 + with: + node-version: "12.x" - name: Get yarn cache directory path id: yarn-cache-dir-path @@ -37,6 +39,8 @@ jobs: steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 + with: + node-version: "12.x" - name: Get yarn cache directory path id: yarn-cache-dir-path diff --git a/docs/preprocessors/in-general.md b/docs/preprocessors/in-general.md index 348279a99..496908688 100644 --- a/docs/preprocessors/in-general.md +++ b/docs/preprocessors/in-general.md @@ -4,7 +4,7 @@ If a svelte file contains some language other than `html`, `css` or `javascript`, `svelte-vscode` needs to know how to [preprocess](https://svelte.dev/docs#svelte_preprocess) it. This can be achieved by creating a `svelte.config.js` file at the root of your project which exports a svelte options object (similar to `svelte-loader` and `rollup-plugin-svelte`). It's recommended to use the official [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess) package which can handle many languages. -> NOTE: you **cannot** use the new `import x from y` and `export const` / `export default` syntax in `svelte.config.js`. +> NOTE: Prior to `svelte-check 1.4.0` / `svelte-language-server 0.13.0` / `Svelte for VS Code 104.9.0` you **cannot** use the new `import x from y` and `export const` / `export default` syntax in `svelte.config.js`. ```js // svelte.config.js diff --git a/packages/language-server/package.json b/packages/language-server/package.json index b088b07e2..a1fb7e8c8 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -1,6 +1,6 @@ { "name": "svelte-language-server", - "version": "0.12.0", + "version": "0.13.0", "description": "A language server for Svelte", "main": "dist/src/index.js", "typings": "dist/src/index", @@ -30,10 +30,13 @@ "url": "https://github.com/sveltejs/language-tools/issues" }, "homepage": "https://github.com/sveltejs/language-tools#readme", + "engines": { + "node": ">= 12.0.0" + }, "devDependencies": { "@tsconfig/node12": "^1.0.0", - "@types/cosmiconfig": "^6.0.0", "@types/estree": "^0.0.42", + "@types/glob": "^7.1.1", "@types/lodash": "^4.14.116", "@types/mocha": "^7.0.2", "@types/node": "^13.9.0", @@ -47,8 +50,8 @@ }, "dependencies": { "chokidar": "^3.4.1", - "cosmiconfig": "^7.0.0", "estree-walker": "^2.0.1", + "glob": "^7.1.6", "lodash": "^4.17.19", "prettier": "2.2.1", "prettier-plugin-svelte": "~2.2.0", diff --git a/packages/language-server/src/lib/documents/Document.ts b/packages/language-server/src/lib/documents/Document.ts index dcfb6ddaa..a03000114 100644 --- a/packages/language-server/src/lib/documents/Document.ts +++ b/packages/language-server/src/lib/documents/Document.ts @@ -2,7 +2,7 @@ import { urlToPath } from '../../utils'; import { WritableDocument } from './DocumentBase'; import { extractScriptTags, extractStyleTag, TagInformation } from './utils'; import { parseHtml } from './parseHtml'; -import { SvelteConfig, loadConfig } from './configLoader'; +import { SvelteConfig, configLoader } from './configLoader'; import { HTMLDocument } from 'vscode-html-languageservice'; /** @@ -13,23 +13,42 @@ export class Document extends WritableDocument { scriptInfo: TagInformation | null = null; moduleScriptInfo: TagInformation | null = null; styleInfo: TagInformation | null = null; - config!: SvelteConfig; + configPromise: Promise; + config?: SvelteConfig; html!: HTMLDocument; constructor(public url: string, public content: string) { super(); + this.configPromise = configLoader.awaitConfig(this.getFilePath() || ''); this.updateDocInfo(); } private updateDocInfo() { - if (!this.config || this.config.loadConfigError) { - this.config = loadConfig(this.getFilePath() || ''); - } this.html = parseHtml(this.content); const scriptTags = extractScriptTags(this.content, this.html); - this.scriptInfo = this.addDefaultLanguage(scriptTags?.script || null, 'script'); - this.moduleScriptInfo = this.addDefaultLanguage(scriptTags?.moduleScript || null, 'script'); - this.styleInfo = this.addDefaultLanguage(extractStyleTag(this.content, this.html), 'style'); + const update = (config: SvelteConfig | undefined) => { + this.config = config; + this.scriptInfo = this.addDefaultLanguage(config, scriptTags?.script || null, 'script'); + this.moduleScriptInfo = this.addDefaultLanguage( + config, + scriptTags?.moduleScript || null, + 'script' + ); + this.styleInfo = this.addDefaultLanguage( + config, + extractStyleTag(this.content, this.html), + 'style' + ); + }; + + const config = configLoader.getConfig(this.getFilePath() || ''); + if (config && !config.loadConfigError) { + update(config); + } else { + this.configPromise = configLoader.awaitConfig(this.getFilePath() || ''); + update(undefined); + this.configPromise.then((c) => update(c)); + } } /** @@ -76,17 +95,19 @@ export class Document extends WritableDocument { } private addDefaultLanguage( + config: SvelteConfig | undefined, tagInfo: TagInformation | null, tag: 'style' | 'script' ): TagInformation | null { - if (!tagInfo) { - return null; + if (!tagInfo || !config) { + return tagInfo; } - const defaultLang = Array.isArray(this.config.preprocess) - ? this.config.preprocess.find((group) => group.defaultLanguages?.[tag]) - ?.defaultLanguages?.[tag] - : this.config.preprocess?.defaultLanguages?.[tag]; + const defaultLang = Array.isArray(config.preprocess) + ? config.preprocess.find((group) => group.defaultLanguages?.[tag])?.defaultLanguages?.[ + tag + ] + : config.preprocess?.defaultLanguages?.[tag]; if (!tagInfo.attributes.lang && !tagInfo.attributes.type && defaultLang) { tagInfo.attributes.lang = defaultLang; diff --git a/packages/language-server/src/lib/documents/configLoader.ts b/packages/language-server/src/lib/documents/configLoader.ts index 3f6dcd9cf..eed448089 100644 --- a/packages/language-server/src/lib/documents/configLoader.ts +++ b/packages/language-server/src/lib/documents/configLoader.ts @@ -1,8 +1,11 @@ import { Logger } from '../../logger'; -import { cosmiconfigSync } from 'cosmiconfig'; import { CompileOptions } from 'svelte/types/compiler/interfaces'; import { PreprocessorGroup } from 'svelte/types/compiler/preprocess/types'; import { importSveltePreprocess } from '../../importPackage'; +import _glob from 'glob'; +import _path from 'path'; +import _fs from 'fs'; +import { pathToFileURL, URL } from 'url'; export type InternalPreprocessorGroup = PreprocessorGroup & { /** @@ -29,54 +32,215 @@ const NO_GENERATE: CompileOptions = { generate: false }; -const svelteConfigExplorer = cosmiconfigSync('svelte', { - packageProp: 'svelte-ls', - cache: true -}); +/** + * 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)') as ( + modulePath: URL +) => Promise; /** - * Tries to load `svelte.config.js` - * - * @param path File path of the document to load the config for + * 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. */ -export function loadConfig(path: string): SvelteConfig { - Logger.log('Trying to load config for', path); - try { - const result = svelteConfigExplorer.search(path); - const config: SvelteConfig = result?.config ?? useFallbackPreprocessor(path, false); - if (result) { - Logger.log('Found config at ', result.filepath); +export class ConfigLoader { + private configFiles = new Map(); + private configFilesAsync = new Map>(); + private filePathToConfigPath = new Map(); + + constructor( + private globSync: typeof _glob.sync, + private fs: Pick, + private path: Pick, + private dynamicImport: typeof _dynamicImport + ) {} + + /** + * 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: string): Promise { + Logger.log('Trying to load configs for', directory); + + try { + const pathResults = this.globSync('**/svelte.config.{js,cjs,mjs}', { + cwd: directory, + ignore: 'node_modules/**' + }); + 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.error(e); } + } + + private addFallbackConfig(directory: string) { + 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); + } + + private searchConfigPathUpwards(path: string) { + let currentDir = path; + let nextDir = this.path.dirname(path); + while (currentDir !== nextDir) { + const tryFindConfigPath = (ending: string) => { + 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); + } + } + + private async loadAndCacheConfig(configPath: string, directory: string) { + 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); + } + } + + private async loadConfig(configPath: string, directory: string) { + try { + let config = (await this.dynamicImport(pathToFileURL(configPath)))?.default; + config = { + ...config, + compilerOptions: { + ...DEFAULT_OPTIONS, + ...config.compilerOptions, + ...NO_GENERATE + } + }; + Logger.log('Loaded config at ', configPath); + return config; + } catch (err) { + Logger.error('Error while loading config'); + 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: string): SvelteConfig | undefined { + 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: string): Promise { + 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); + } + + private tryGetConfig(file: string, fromDirectory: string, configFileEnding: string) { + const path = this.path.join(fromDirectory, `svelte.config.${configFileEnding}`); + const config = this.configFiles.get(path); + if (config) { + this.filePathToConfigPath.set(file, path); + return config; + } + } + + private useFallbackPreprocessor(path: string, foundConfig: boolean): SvelteConfig { + 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' + ); + const sveltePreprocess = importSveltePreprocess(path); return { - ...config, - compilerOptions: { ...DEFAULT_OPTIONS, ...config.compilerOptions, ...NO_GENERATE } - }; - } catch (err) { - Logger.error('Error while loading config'); - Logger.error(err); - return { - ...useFallbackPreprocessor(path, true), - compilerOptions: { - ...DEFAULT_OPTIONS, - ...NO_GENERATE - }, - loadConfigError: err + 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 } } + }) }; } } -function useFallbackPreprocessor(path: string, foundConfig: boolean): SvelteConfig { - 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: importSveltePreprocess(path)({ - // 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 } } - }) - }; -} +export const configLoader = new ConfigLoader(_glob.sync, _fs, _path, _dynamicImport); diff --git a/packages/language-server/src/plugins/svelte/SvelteDocument.ts b/packages/language-server/src/plugins/svelte/SvelteDocument.ts index 3df4343b6..d253fca79 100644 --- a/packages/language-server/src/plugins/svelte/SvelteDocument.ts +++ b/packages/language-server/src/plugins/svelte/SvelteDocument.ts @@ -42,7 +42,9 @@ export class SvelteDocument { public languageId = 'svelte'; public version = 0; public uri = this.parent.uri; - public config = this.parent.config; + public get config() { + return this.parent.configPromise; + } constructor(private parent: Document) { this.script = this.parent.scriptInfo; @@ -67,7 +69,7 @@ export class SvelteDocument { if (!this.transpiledDoc) { this.transpiledDoc = await TranspiledSvelteDocument.create( this.parent, - this.parent.config.preprocess + (await this.config)?.preprocess ); } return this.transpiledDoc; @@ -75,7 +77,7 @@ export class SvelteDocument { async getCompiled(): Promise { if (!this.compileResult) { - this.compileResult = await this.getCompiledWith(this.parent.config.compilerOptions); + this.compileResult = await this.getCompiledWith((await this.config)?.compilerOptions); } return this.compileResult; diff --git a/packages/language-server/src/plugins/svelte/features/getDiagnostics.ts b/packages/language-server/src/plugins/svelte/features/getDiagnostics.ts index 6f156e08e..54cbb5f1e 100644 --- a/packages/language-server/src/plugins/svelte/features/getDiagnostics.ts +++ b/packages/language-server/src/plugins/svelte/features/getDiagnostics.ts @@ -14,8 +14,9 @@ export async function getDiagnostics( svelteDoc: SvelteDocument, settings: CompilerWarningsSettings ): Promise { - if (svelteDoc.config.loadConfigError) { - return getConfigLoadErrorDiagnostics(svelteDoc.config.loadConfigError); + const config = await svelteDoc.config; + if (config?.loadConfigError) { + return getConfigLoadErrorDiagnostics(config.loadConfigError); } try { diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index 7d536bc8b..6772a95c7 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -59,51 +59,51 @@ export class LSAndTSDocResolver { }; } - getLSForPath(path: string) { + async getLSForPath(path: string) { return getLanguageServiceForPath(path, this.workspaceUris, this.lsDocumentContext); } - getLSAndTSDoc( + async getLSAndTSDoc( document: Document - ): { + ): Promise<{ tsDoc: SvelteDocumentSnapshot; lang: ts.LanguageService; userPreferences: ts.UserPreferences; - } { - const lang = getLanguageServiceForDocument( + }> { + const lang = await getLanguageServiceForDocument( document, this.workspaceUris, this.lsDocumentContext ); - const tsDoc = this.getSnapshot(document); + const tsDoc = await this.getSnapshot(document); const userPreferences = this.getUserPreferences(tsDoc.scriptKind); return { tsDoc, lang, userPreferences }; } - getSnapshot(document: Document): SvelteDocumentSnapshot; - getSnapshot(pathOrDoc: string | Document): DocumentSnapshot; - getSnapshot(pathOrDoc: string | Document) { + async getSnapshot(document: Document): Promise; + async getSnapshot(pathOrDoc: string | Document): Promise; + async getSnapshot(pathOrDoc: string | Document) { const filePath = typeof pathOrDoc === 'string' ? pathOrDoc : pathOrDoc.getFilePath() || ''; - const tsService = this.getTSService(filePath); + const tsService = await this.getTSService(filePath); return tsService.updateDocument(pathOrDoc); } - updateSnapshotPath(oldPath: string, newPath: string): DocumentSnapshot { - this.deleteSnapshot(oldPath); + async updateSnapshotPath(oldPath: string, newPath: string): Promise { + await this.deleteSnapshot(oldPath); return this.getSnapshot(newPath); } - deleteSnapshot(filePath: string) { - this.getTSService(filePath).deleteDocument(filePath); + async deleteSnapshot(filePath: string) { + (await this.getTSService(filePath)).deleteDocument(filePath); this.docManager.releaseDocument(pathToUrl(filePath)); } - getSnapshotManager(filePath: string): SnapshotManager { - return this.getTSService(filePath).snapshotManager; + async getSnapshotManager(filePath: string): Promise { + return (await this.getTSService(filePath)).snapshotManager; } - private getTSService(filePath: string): LanguageServiceContainer { + private getTSService(filePath: string): Promise { return getService(filePath, this.workspaceUris, this.lsDocumentContext); } diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index a986141ec..a0d56d669 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -146,7 +146,7 @@ export class TypeScriptPlugin return []; } - const { lang, tsDoc } = this.getLSAndTSDoc(document); + const { lang, tsDoc } = await this.getLSAndTSDoc(document); const fragment = await tsDoc.getFragment(); const navTree = lang.getNavigationTree(tsDoc.filePath); @@ -262,7 +262,7 @@ export class TypeScriptPlugin return []; } - const { lang, tsDoc } = this.getLSAndTSDoc(document); + const { lang, tsDoc } = await this.getLSAndTSDoc(document); const mainFragment = await tsDoc.getFragment(); const defs = lang.getDefinitionAndBoundSpan( @@ -363,7 +363,7 @@ export class TypeScriptPlugin return this.findReferencesProvider.findReferences(document, position, context); } - onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]) { + async onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): Promise { const doneUpdateProjectFiles = new Set(); for (const { fileName, changeType } of onWatchFileChangesParas) { @@ -374,7 +374,7 @@ export class TypeScriptPlugin continue; } - const snapshotManager = this.getSnapshotManager(fileName); + const snapshotManager = await this.getSnapshotManager(fileName); if (changeType === FileChangeType.Created) { if (!doneUpdateProjectFiles.has(snapshotManager)) { snapshotManager.updateProjectFiles(); @@ -389,8 +389,11 @@ export class TypeScriptPlugin } } - updateTsOrJsFile(fileName: string, changes: TextDocumentContentChangeEvent[]): void { - const snapshotManager = this.getSnapshotManager(fileName); + async updateTsOrJsFile( + fileName: string, + changes: TextDocumentContentChangeEvent[] + ): Promise { + const snapshotManager = await this.getSnapshotManager(fileName); snapshotManager.updateTsOrJsFile(fileName, changes); } @@ -427,7 +430,7 @@ export class TypeScriptPlugin return this.semanticTokensProvider.getSemanticTokens(textDocument, range); } - private getLSAndTSDoc(document: Document) { + private async getLSAndTSDoc(document: Document) { return this.lsAndTsDocResolver.getLSAndTSDoc(document); } diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts index 3471917a1..718739e73 100644 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts @@ -66,7 +66,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { return []; } - const { lang, tsDoc, userPreferences } = this.getLSAndTSDoc(document); + const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document); const fragment = await tsDoc.getFragment(); const changes = lang.organizeImports( @@ -145,7 +145,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { } private async applyQuickfix(document: Document, range: Range, context: CodeActionContext) { - const { lang, tsDoc, userPreferences } = this.getLSAndTSDoc(document); + const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document); const fragment = await tsDoc.getFragment(); const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start)); @@ -252,7 +252,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { return []; } - const { lang, tsDoc, userPreferences } = this.getLSAndTSDoc(document); + const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document); const fragment = await tsDoc.getFragment(); const textRange = { pos: fragment.offsetAt(fragment.getGeneratedPosition(range.start)), @@ -343,7 +343,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { return null; } - const { lang, tsDoc, userPreferences } = this.getLSAndTSDoc(document); + const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document); const fragment = await tsDoc.getFragment(); const path = document.getFilePath() || ''; const { refactorName, originalRange, textRange } = args[1]; @@ -395,7 +395,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { return resultRange; } - private getLSAndTSDoc(document: Document) { + private async getLSAndTSDoc(document: Document) { return this.lsAndTsDocResolver.getLSAndTSDoc(document); } } diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index 1620a4a24..4d8663048 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -63,7 +63,9 @@ export class CompletionsProviderImpl implements CompletionsProvider> { - const snapshot = getComponentAtPosition( + ): Promise>> { + const snapshot = await getComponentAtPosition( this.lsAndTsDocResolver, lang, doc, @@ -272,7 +274,9 @@ export class CompletionsProviderImpl implements CompletionsProvider ): Promise> { const { data: comp } = completionItem; - const { tsDoc, lang, userPreferences } = this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { tsDoc, lang, userPreferences } = await this.lsAndTsDocResolver.getLSAndTSDoc( + document + ); const filePath = tsDoc.filePath; diff --git a/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts b/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts index 71fe66080..33c0771f7 100644 --- a/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/DiagnosticsProvider.ts @@ -11,7 +11,7 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider { constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} async getDiagnostics(document: Document): Promise { - const { lang, tsDoc } = this.getLSAndTSDoc(document); + const { lang, tsDoc } = await this.getLSAndTSDoc(document); if (['coffee', 'coffeescript'].includes(document.getLanguageAttribute('script'))) { return []; @@ -68,7 +68,7 @@ export class DiagnosticsProviderImpl implements DiagnosticsProvider { return tags; } - private getLSAndTSDoc(document: Document) { + private async getLSAndTSDoc(document: Document) { return this.lsAndTsDocResolver.getLSAndTSDoc(document); } } diff --git a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts b/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts index 21a70c6d7..8957d5403 100644 --- a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts @@ -15,7 +15,7 @@ export class FindReferencesProviderImpl implements FindReferencesProvider { position: Position, context: ReferenceContext ): Promise { - const { lang, tsDoc } = this.getLSAndTSDoc(document); + const { lang, tsDoc } = await this.getLSAndTSDoc(document); const fragment = await tsDoc.getFragment(); const references = lang.getReferencesAtPosition( @@ -44,7 +44,7 @@ export class FindReferencesProviderImpl implements FindReferencesProvider { ); } - private getLSAndTSDoc(document: Document) { + private async getLSAndTSDoc(document: Document) { return this.lsAndTsDocResolver.getLSAndTSDoc(document); } } diff --git a/packages/language-server/src/plugins/typescript/features/HoverProvider.ts b/packages/language-server/src/plugins/typescript/features/HoverProvider.ts index 1f99dfdf0..5ddc4e6a7 100644 --- a/packages/language-server/src/plugins/typescript/features/HoverProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/HoverProvider.ts @@ -12,10 +12,16 @@ export class HoverProviderImpl implements HoverProvider { constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} async doHover(document: Document, position: Position): Promise { - const { lang, tsDoc } = this.getLSAndTSDoc(document); + const { lang, tsDoc } = await this.getLSAndTSDoc(document); const fragment = await tsDoc.getFragment(); - const eventHoverInfo = this.getEventHoverInfo(lang, document, tsDoc, fragment, position); + const eventHoverInfo = await this.getEventHoverInfo( + lang, + document, + tsDoc, + fragment, + position + ); if (eventHoverInfo) { return eventHoverInfo; } @@ -56,13 +62,13 @@ export class HoverProviderImpl implements HoverProvider { }); } - private getEventHoverInfo( + private async getEventHoverInfo( lang: ts.LanguageService, doc: Document, tsDoc: SvelteDocumentSnapshot, fragment: SvelteSnapshotFragment, originalPosition: Position - ): Hover | null { + ): Promise { const possibleEventName = getWordAt(doc.getText(), doc.offsetAt(originalPosition), { left: /\S+$/, right: /[\s=]/ @@ -71,7 +77,7 @@ export class HoverProviderImpl implements HoverProvider { return null; } - const component = getComponentAtPosition( + const component = await getComponentAtPosition( this.lsAndTsDocResolver, lang, doc, @@ -99,7 +105,7 @@ export class HoverProviderImpl implements HoverProvider { }; } - private getLSAndTSDoc(document: Document) { + private async getLSAndTSDoc(document: Document) { return this.lsAndTsDocResolver.getLSAndTSDoc(document); } } diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts index e0f807e44..4b263cc16 100644 --- a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts @@ -6,7 +6,7 @@ import { offsetAt, getLineAtPosition } from '../../../lib/documents'; -import { isNotNullOrUndefined, pathToUrl } from '../../../utils'; +import { filterAsync, isNotNullOrUndefined, pathToUrl } from '../../../utils'; import { RenameProvider } from '../../interfaces'; import { SnapshotFragment, @@ -25,7 +25,7 @@ export class RenameProviderImpl implements RenameProvider { // TODO props written as `export {x as y}` are not supported yet. async prepareRename(document: Document, position: Position): Promise { - const { lang, tsDoc } = this.getLSAndTSDoc(document); + const { lang, tsDoc } = await this.getLSAndTSDoc(document); const fragment = await tsDoc.getFragment(); const offset = fragment.offsetAt(fragment.getGeneratedPosition(position)); @@ -42,7 +42,7 @@ export class RenameProviderImpl implements RenameProvider { position: Position, newName: string ): Promise { - const { lang, tsDoc } = this.getLSAndTSDoc(document); + const { lang, tsDoc } = await this.getLSAndTSDoc(document); const fragment = await tsDoc.getFragment(); const offset = fragment.offsetAt(fragment.getGeneratedPosition(position)); @@ -316,9 +316,9 @@ export class RenameProviderImpl implements RenameProvider { private filterWrongRenameLocations( mappedLocations: Array - ): Array { - return mappedLocations.filter((loc) => { - const snapshot = this.getSnapshot(loc.fileName); + ): Promise> { + return filterAsync(mappedLocations, async (loc) => { + const snapshot = await this.getSnapshot(loc.fileName); if (!(snapshot instanceof SvelteDocumentSnapshot)) { return true; } @@ -370,7 +370,7 @@ export class RenameProviderImpl implements RenameProvider { return tsDoc.getText(start, start + length); } - private getLSAndTSDoc(document: Document) { + private async getLSAndTSDoc(document: Document) { return this.lsAndTsDocResolver.getLSAndTSDoc(document); } diff --git a/packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts b/packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts index 462fdced4..355abc4a6 100644 --- a/packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/SelectionRangeProvider.ts @@ -13,7 +13,7 @@ export class SelectionRangeProviderImpl implements SelectionRangeProvider { document: Document, position: Position ): Promise { - const { tsDoc, lang } = this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { tsDoc, lang } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); const fragment = await tsDoc.getFragment(); const tsSelectionRange = lang.getSmartSelectionRange( diff --git a/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts b/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts index a588dbfcd..d2e3e7061 100644 --- a/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts @@ -12,7 +12,7 @@ export class SemanticTokensProviderImpl implements SemanticTokensProvider { constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} async getSemanticTokens(textDocument: Document, range?: Range): Promise { - const { lang, tsDoc } = this.lsAndTsDocResolver.getLSAndTSDoc(textDocument); + const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(textDocument); const fragment = await tsDoc.getFragment(); // for better performance, don't do full-file semantic tokens when the file is too big diff --git a/packages/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts b/packages/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts index daa2e44be..82d900c77 100644 --- a/packages/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts @@ -24,7 +24,7 @@ export class SignatureHelpProviderImpl implements SignatureHelpProvider { position: Position, context: SignatureHelpContext | undefined ): Promise { - const { lang, tsDoc } = this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); const fragment = await tsDoc.getFragment(); const offset = fragment.offsetAt(fragment.getGeneratedPosition(position)); diff --git a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts index d473f9d2d..ea20f91c7 100644 --- a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts @@ -21,11 +21,11 @@ export class UpdateImportsProviderImpl implements UpdateImportsProvider { return null; } - const ls = this.getLSForPath(newPath); + const ls = await this.getLSForPath(newPath); // `getEditsForFileRename` might take a while const fileChanges = ls.getEditsForFileRename(oldPath, newPath, {}, {}); - this.lsAndTsDocResolver.updateSnapshotPath(oldPath, newPath); + await this.lsAndTsDocResolver.updateSnapshotPath(oldPath, newPath); const updateImportsChanges = fileChanges // Assumption: Updating imports will not create new files, and to make sure just filter those out // who - for whatever reason - might be new ones. @@ -58,7 +58,7 @@ export class UpdateImportsProviderImpl implements UpdateImportsProvider { return { documentChanges }; } - private getLSForPath(path: string) { + private async getLSForPath(path: string) { return this.lsAndTsDocResolver.getLSForPath(path); } } diff --git a/packages/language-server/src/plugins/typescript/features/utils.ts b/packages/language-server/src/plugins/typescript/features/utils.ts index fc014dd4a..d96ba6ab7 100644 --- a/packages/language-server/src/plugins/typescript/features/utils.ts +++ b/packages/language-server/src/plugins/typescript/features/utils.ts @@ -13,14 +13,14 @@ import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; * If the given original position is within a Svelte starting tag, * return the snapshot of that component. */ -export function getComponentAtPosition( +export async function getComponentAtPosition( lsAndTsDocResovler: LSAndTSDocResolver, lang: ts.LanguageService, doc: Document, tsDoc: SvelteDocumentSnapshot, fragment: SvelteSnapshotFragment, originalPosition: Position -): SvelteDocumentSnapshot | null { +): Promise { if (tsDoc.parserError) { return null; } @@ -47,7 +47,7 @@ export function getComponentAtPosition( return null; } - const snapshot = lsAndTsDocResovler.getSnapshot(def.fileName); + const snapshot = await lsAndTsDocResovler.getSnapshot(def.fileName); if (!(snapshot instanceof SvelteDocumentSnapshot)) { return null; } @@ -94,7 +94,7 @@ export class SnapshotFragmentMap { async retrieve(fileName: string) { let snapshotFragment = this.get(fileName); if (!snapshotFragment) { - const snapshot = this.resolver.getSnapshot(fileName); + const snapshot = await this.resolver.getSnapshot(fileName); const fragment = await snapshot.getFragment(); snapshotFragment = { fragment, snapshot }; this.set(fileName, snapshotFragment); diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 2d13e0d8b..1a44727c6 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -7,6 +7,7 @@ import { DocumentSnapshot } from './DocumentSnapshot'; import { createSvelteModuleLoader } from './module-loader'; import { SnapshotManager } from './SnapshotManager'; import { ensureRealSvelteFilePath, findTsConfigPath } from './utils'; +import { configLoader } from '../../lib/documents/configLoader'; export interface LanguageServiceContainer { readonly tsconfigPath: string; @@ -17,30 +18,30 @@ export interface LanguageServiceContainer { deleteDocument(filePath: string): void; } -const services = new Map(); +const services = new Map>(); export interface LanguageServiceDocumentContext { transformOnTemplateError: boolean; createDocument: (fileName: string, content: string) => Document; } -export function getLanguageServiceForPath( +export async function getLanguageServiceForPath( path: string, workspaceUris: string[], docContext: LanguageServiceDocumentContext -): ts.LanguageService { - return getService(path, workspaceUris, docContext).getService(); +): Promise { + return (await getService(path, workspaceUris, docContext)).getService(); } -export function getLanguageServiceForDocument( +export async function getLanguageServiceForDocument( document: Document, workspaceUris: string[], docContext: LanguageServiceDocumentContext -): ts.LanguageService { +): Promise { return getLanguageServiceForPath(document.getFilePath() || '', workspaceUris, docContext); } -export function getService( +export async function getService( path: string, workspaceUris: string[], docContext: LanguageServiceDocumentContext @@ -49,20 +50,21 @@ export function getService( let service: LanguageServiceContainer; if (services.has(tsconfigPath)) { - service = services.get(tsconfigPath)!; + service = await services.get(tsconfigPath)!; } else { Logger.log('Initialize new ts service at ', tsconfigPath); - service = createLanguageService(tsconfigPath, docContext); - services.set(tsconfigPath, service); + const newService = createLanguageService(tsconfigPath, docContext); + services.set(tsconfigPath, newService); + service = await newService; } return service; } -export function createLanguageService( +async function createLanguageService( tsconfigPath: string, docContext: LanguageServiceDocumentContext -): LanguageServiceContainer { +): Promise { const workspacePath = tsconfigPath ? dirname(tsconfigPath) : ''; const { options: compilerOptions, fileNames: files, raw } = getParsedConfig(); @@ -70,6 +72,11 @@ export function createLanguageService( // see: https://github.com/microsoft/TypeScript/blob/08e4f369fbb2a5f0c30dee973618d65e6f7f09f8/src/compiler/commandLineParser.ts#L2537 const snapshotManager = new SnapshotManager(files, raw, workspacePath || process.cwd()); + // 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 to determine + // the default language. + await configLoader.loadConfigs(workspacePath); + const svelteModuleLoader = createSvelteModuleLoader(getSnapshot, compilerOptions); let svelteTsPath: string; @@ -148,13 +155,13 @@ export function createLanguageService( docContext.createDocument, 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 = ts.createLanguageService(host); } - snapshotManager.set(filePath, newSnapshot); return newSnapshot; } diff --git a/packages/language-server/src/utils.ts b/packages/language-server/src/utils.ts index be736d82e..ef0ebbebd 100644 --- a/packages/language-server/src/utils.ts +++ b/packages/language-server/src/utils.ts @@ -126,3 +126,18 @@ export function modifyLines( ) .join('\r\n'); } + +/** + * Like array.filter, but asynchronous + */ +export async function filterAsync( + array: T[], + predicate: (t: T, idx: number) => Promise +): Promise { + const fail = Symbol(); + return ( + await Promise.all( + array.map(async (item, idx) => ((await predicate(item, idx)) ? item : fail)) + ) + ).filter((i) => i !== fail) as T[]; +} diff --git a/packages/language-server/test/lib/documents/configLoader.test.ts b/packages/language-server/test/lib/documents/configLoader.test.ts new file mode 100644 index 000000000..676b999ec --- /dev/null +++ b/packages/language-server/test/lib/documents/configLoader.test.ts @@ -0,0 +1,149 @@ +import { ConfigLoader } from '../../../src/lib/documents/configLoader'; +import path from 'path'; +import { pathToFileURL, URL } from 'url'; +import assert from 'assert'; + +describe('ConfigLoader', () => { + function configFrom(path: string) { + return { + compilerOptions: { + dev: true, + generate: false + }, + preprocess: pathToFileURL(path).toString() + }; + } + + async function assertFindsConfig( + configLoader: ConfigLoader, + filePath: string, + configPath: string + ) { + filePath = path.join(...filePath.split('/')); + configPath = path.join(...configPath.split('/')); + assert.deepStrictEqual(configLoader.getConfig(filePath), configFrom(configPath)); + assert.deepStrictEqual(await configLoader.awaitConfig(filePath), configFrom(configPath)); + } + + it('should load all config files below and the one inside/above given directory', async () => { + const configLoader = new ConfigLoader( + () => ['svelte.config.js', 'below/svelte.config.js'], + { existsSync: () => true }, + path, + (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } }) + ); + await configLoader.loadConfigs('/some/path'); + + assertFindsConfig(configLoader, '/some/path/comp.svelte', '/some/path/svelte.config.js'); + assertFindsConfig( + configLoader, + '/some/path/aside/comp.svelte', + '/some/path/svelte.config.js' + ); + assertFindsConfig( + configLoader, + '/some/path/below/comp.svelte', + '/some/path/below/svelte.config.js' + ); + assertFindsConfig( + configLoader, + '/some/path/below/further/comp.svelte', + '/some/path/below/svelte.config.js' + ); + }); + + it('finds first above if none found inside/below directory', async () => { + const configLoader = new ConfigLoader( + () => [], + { + existsSync: (p) => + typeof p === 'string' && p.endsWith(path.join('some', 'svelte.config.js')) + }, + path, + (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } }) + ); + await configLoader.loadConfigs('/some/path'); + + assertFindsConfig(configLoader, '/some/path/comp.svelte', '/some/svelte.config.js'); + }); + + it('adds fallback if no config found', async () => { + const configLoader = new ConfigLoader( + () => [], + { existsSync: () => false }, + path, + (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } }) + ); + await configLoader.loadConfigs('/some/path'); + + assert.deepStrictEqual( + // Can't do the equal-check directly, instead check if it's the expected object props + // of svelte-preprocess + Object.keys(configLoader.getConfig('/some/path/comp.svelte')?.preprocess || {}).sort(), + ['defaultLanguages', 'markup', 'script', 'style'].sort() + ); + }); + + it('will not load config multiple times if config loading started in parallel', async () => { + let firstGlobCall = true; + let nrImportCalls = 0; + const configLoader = new ConfigLoader( + () => { + if (firstGlobCall) { + firstGlobCall = false; + return ['svelte.config.js']; + } else { + return []; + } + }, + { + existsSync: (p) => + typeof p === 'string' && + p.endsWith(path.join('some', 'path', 'svelte.config.js')) + }, + path, + (module: URL) => { + nrImportCalls++; + return new Promise((resolve) => { + setTimeout(() => resolve({ default: { preprocess: module.toString() } }), 500); + }); + } + ); + await Promise.all([ + configLoader.loadConfigs('/some/path'), + configLoader.loadConfigs('/some/path/sub'), + configLoader.awaitConfig('/some/path/file.svelte') + ]); + + assertFindsConfig(configLoader, '/some/path/comp.svelte', '/some/path/svelte.config.js'); + assertFindsConfig( + configLoader, + '/some/path/sub/comp.svelte', + '/some/path/svelte.config.js' + ); + assert.deepStrictEqual(nrImportCalls, 1); + }); + + it('can deal with missing config', () => { + const configLoader = new ConfigLoader( + () => [], + { existsSync: () => false }, + path, + () => Promise.resolve('unimportant') + ); + assert.deepStrictEqual(configLoader.getConfig('/some/file.svelte'), undefined); + }); + + it('should await config', async () => { + const configLoader = new ConfigLoader( + () => [], + { existsSync: () => true }, + path, + (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } }) + ); + assert.deepStrictEqual( + await configLoader.awaitConfig(path.join('some', 'file.svelte')), + configFrom(path.join('some', 'svelte.config.js')) + ); + }); +}); diff --git a/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts b/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts index d14ac60f8..02d24cc7d 100644 --- a/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts +++ b/packages/language-server/test/plugins/svelte/SvelteDocument.test.ts @@ -7,7 +7,7 @@ import { SvelteDocument, TranspiledSvelteDocument } from '../../../src/plugins/svelte/SvelteDocument'; -import * as configLoader from '../../../src/lib/documents/configLoader'; +import { configLoader, SvelteConfig } from '../../../src/lib/documents/configLoader'; describe('Svelte Document', () => { function getSourceCode(transpiled: boolean): string { @@ -19,8 +19,8 @@ describe('Svelte Document', () => { `; } - function setup(config: configLoader.SvelteConfig = {}) { - sinon.stub(configLoader, 'loadConfig').returns(config); + function setup(config: SvelteConfig = {}) { + sinon.stub(configLoader, 'getConfig').returns(config); const parent = new Document('file:///hello.svelte', getSourceCode(false)); sinon.restore(); const svelteDoc = new SvelteDocument(parent); diff --git a/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts b/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts index 78875168f..c5bf39f5a 100644 --- a/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts +++ b/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts @@ -347,10 +347,10 @@ describe('TypescriptPlugin', () => { }); }); - const setupForOnWatchedFileChanges = () => { + const setupForOnWatchedFileChanges = async () => { const { plugin, document } = setup('empty.svelte'); const targetSvelteFile = document.getFilePath()!; - const snapshotManager = plugin.getSnapshotManager(targetSvelteFile); + const snapshotManager = await plugin.getSnapshotManager(targetSvelteFile); return { snapshotManager, @@ -366,14 +366,14 @@ describe('TypescriptPlugin', () => { return urlToPath(pathToUrl(path)) ?? ''; }; - const setupForOnWatchedFileUpdateOrDelete = () => { - const { plugin, snapshotManager, targetSvelteFile } = setupForOnWatchedFileChanges(); + const setupForOnWatchedFileUpdateOrDelete = async () => { + const { plugin, snapshotManager, targetSvelteFile } = await setupForOnWatchedFileChanges(); const projectJsFile = normalizeWatchFilePath( path.join(path.dirname(targetSvelteFile), 'documentation.ts') ); - plugin.onWatchFileChanges([ + await plugin.onWatchFileChanges([ { fileName: projectJsFile, changeType: FileChangeType.Changed @@ -387,15 +387,19 @@ describe('TypescriptPlugin', () => { }; }; - it('bumps snapshot version when watched file changes', () => { - const { snapshotManager, projectJsFile, plugin } = setupForOnWatchedFileUpdateOrDelete(); + it('bumps snapshot version when watched file changes', async () => { + const { + snapshotManager, + projectJsFile, + plugin + } = await setupForOnWatchedFileUpdateOrDelete(); const firstSnapshot = snapshotManager.get(projectJsFile); const firstVersion = firstSnapshot?.version; assert.notEqual(firstVersion, INITIAL_VERSION); - plugin.onWatchFileChanges([ + await plugin.onWatchFileChanges([ { fileName: projectJsFile, changeType: FileChangeType.Changed @@ -406,13 +410,17 @@ describe('TypescriptPlugin', () => { assert.notEqual(secondSnapshot?.version, firstVersion); }); - it('should delete snapshot cache when file delete', () => { - const { snapshotManager, projectJsFile, plugin } = setupForOnWatchedFileUpdateOrDelete(); + it('should delete snapshot cache when file delete', async () => { + const { + snapshotManager, + projectJsFile, + plugin + } = await setupForOnWatchedFileUpdateOrDelete(); const firstSnapshot = snapshotManager.get(projectJsFile); assert.notEqual(firstSnapshot, undefined); - plugin.onWatchFileChanges([ + await plugin.onWatchFileChanges([ { fileName: projectJsFile, changeType: FileChangeType.Deleted @@ -423,8 +431,8 @@ describe('TypescriptPlugin', () => { assert.equal(secondSnapshot, undefined); }); - it('should add snapshot when project file added', () => { - const { snapshotManager, plugin, targetSvelteFile } = setupForOnWatchedFileChanges(); + it('should add snapshot when project file added', async () => { + const { snapshotManager, plugin, targetSvelteFile } = await setupForOnWatchedFileChanges(); const addFile = path.join(path.dirname(targetSvelteFile), 'foo.ts'); const normalizedAddFilePath = normalizeWatchFilePath(addFile); @@ -432,7 +440,7 @@ describe('TypescriptPlugin', () => { fs.writeFileSync(addFile, 'export function abc() {}'); assert.equal(snapshotManager.has(normalizedAddFilePath), false); - plugin.onWatchFileChanges([ + await plugin.onWatchFileChanges([ { fileName: normalizedAddFilePath, changeType: FileChangeType.Created @@ -445,8 +453,12 @@ describe('TypescriptPlugin', () => { } }); - it('should update ts/js file after document change', () => { - const { snapshotManager, projectJsFile, plugin } = setupForOnWatchedFileUpdateOrDelete(); + it('should update ts/js file after document change', async () => { + const { + snapshotManager, + projectJsFile, + plugin + } = await setupForOnWatchedFileUpdateOrDelete(); const firstSnapshot = snapshotManager.get(projectJsFile); const firstVersion = firstSnapshot?.version; @@ -454,7 +466,7 @@ describe('TypescriptPlugin', () => { assert.notEqual(firstVersion, INITIAL_VERSION); - plugin.updateTsOrJsFile(projectJsFile, [ + await plugin.updateTsOrJsFile(projectJsFile, [ { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, text: 'const = "hello world";' diff --git a/packages/language-server/test/plugins/typescript/features/UpdateImportsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/UpdateImportsProvider.test.ts index b460a8b73..a79e405e9 100644 --- a/packages/language-server/test/plugins/typescript/features/UpdateImportsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/UpdateImportsProvider.test.ts @@ -19,7 +19,7 @@ const testDir = join(__dirname, '..'); const testFilesDir = join(testDir, 'testfiles'); describe('UpdateImportsProviderImpl', () => { - function setup(filename: string) { + async function setup(filename: string) { const docManager = new DocumentManager( (textDocument) => new Document(textDocument.uri, textDocument.text) ); @@ -35,14 +35,14 @@ describe('UpdateImportsProviderImpl', () => { uri: fileUri, text: ts.sys.readFile(filePath) || '' }); - lsAndTsDocResolver.getLSAndTSDoc(document); // this makes sure ts ls knows the file + await lsAndTsDocResolver.getLSAndTSDoc(document); // this makes sure ts ls knows the file return { updateImportsProvider, fileUri }; } afterEach(() => sinon.restore()); it('updates imports', async () => { - const { updateImportsProvider, fileUri } = setup('updateimports.svelte'); + const { updateImportsProvider, fileUri } = await setup('updateimports.svelte'); const workspaceEdit = await updateImportsProvider.updateImports({ // imported files both old and new have to actually exist, so we just use some other test files