diff --git a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts index d770daf53..285c30c45 100644 --- a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts +++ b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts @@ -96,12 +96,13 @@ export namespace DocumentSnapshot { export function fromFilePath( filePath: string, createDocument: (filePath: string, text: string) => Document, - options: SvelteSnapshotOptions + options: SvelteSnapshotOptions, + tsSystem: ts.System ) { if (isSvelteFilePath(filePath)) { return DocumentSnapshot.fromSvelteFilePath(filePath, createDocument, options); } else { - return DocumentSnapshot.fromNonSvelteFilePath(filePath); + return DocumentSnapshot.fromNonSvelteFilePath(filePath, tsSystem); } } @@ -110,7 +111,7 @@ export namespace DocumentSnapshot { * @param filePath path to the js/ts file * @param options options that apply in case it's a svelte file */ - export function fromNonSvelteFilePath(filePath: string) { + export function fromNonSvelteFilePath(filePath: string, tsSystem: ts.System) { let originalText = ''; // The following (very hacky) code makes sure that the ambient module definitions @@ -121,7 +122,7 @@ export namespace DocumentSnapshot { // on their own. const normalizedPath = filePath.replace(/\\/g, '/'); if (!normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) { - originalText = ts.sys.readFile(filePath) || ''; + originalText = tsSystem.readFile(filePath) || ''; } if ( normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts') || diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index f926289ae..2f9cafe7d 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -1,3 +1,4 @@ +import { dirname } from 'path'; import ts from 'typescript'; import { TextDocumentContentChangeEvent } from 'vscode-languageserver'; import { Document, DocumentManager } from '../../lib/documents'; @@ -27,6 +28,7 @@ interface LSAndTSDocResolverOptions { onProjectReloaded?: () => void; watchTsConfig?: boolean; + tsSystem?: ts.System; } export class LSAndTSDocResolver { @@ -69,7 +71,7 @@ export class LSAndTSDocResolver { return document; }; - private globalSnapshotsManager = new GlobalSnapshotsManager(); + private globalSnapshotsManager = new GlobalSnapshotsManager(this.lsDocumentContext.tsSystem); private extendedConfigCache = new Map(); private get lsDocumentContext(): LanguageServiceDocumentContext { @@ -83,7 +85,7 @@ export class LSAndTSDocResolver { extendedConfigCache: this.extendedConfigCache, onProjectReloaded: this.options?.onProjectReloaded, watchTsConfig: !!this.options?.watchTsConfig, - tsSystem: ts.sys + tsSystem: this.options?.tsSystem ?? ts.sys }; } @@ -168,7 +170,11 @@ export class LSAndTSDocResolver { async getTSService(filePath?: string): Promise { if (this.options?.tsconfigPath) { - return getServiceForTsconfig(this.options?.tsconfigPath, this.lsDocumentContext); + return getServiceForTsconfig( + this.options?.tsconfigPath, + dirname(this.options.tsconfigPath), + this.lsDocumentContext + ); } if (!filePath) { throw new Error('Cannot call getTSService without filePath and without tsconfigPath'); diff --git a/packages/language-server/src/plugins/typescript/SnapshotManager.ts b/packages/language-server/src/plugins/typescript/SnapshotManager.ts index 23c73c404..e5fb6b57b 100644 --- a/packages/language-server/src/plugins/typescript/SnapshotManager.ts +++ b/packages/language-server/src/plugins/typescript/SnapshotManager.ts @@ -16,6 +16,8 @@ export class GlobalSnapshotsManager { private emitter = new EventEmitter(); private documents = new Map(); + constructor(private readonly tsSystem: ts.System) {} + get(fileName: string) { fileName = normalizePath(fileName); return this.documents.get(fileName); @@ -48,7 +50,7 @@ export class GlobalSnapshotsManager { this.emitter.emit('change', fileName, previousSnapshot); return previousSnapshot; } else { - const newSnapshot = DocumentSnapshot.fromNonSvelteFilePath(fileName); + const newSnapshot = DocumentSnapshot.fromNonSvelteFilePath(fileName, this.tsSystem); if (previousSnapshot) { newSnapshot.version = previousSnapshot.version + 1; diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index 7a31fbf33..e9eda52a7 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -121,9 +121,10 @@ class ImpliedNodeFormatResolver { */ export function createSvelteModuleLoader( getSnapshot: (fileName: string) => DocumentSnapshot, - compilerOptions: ts.CompilerOptions + compilerOptions: ts.CompilerOptions, + tsSystem: ts.System ) { - const svelteSys = createSvelteSys(getSnapshot); + const svelteSys = createSvelteSys(getSnapshot, tsSystem); const moduleCache = new ModuleResolutionCache(); const impliedNodeFormatResolver = new ImpliedNodeFormatResolver(); diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 2ad397c22..18d514a2c 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -5,7 +5,7 @@ import { getPackageInfo } from '../../importPackage'; import { Document } from '../../lib/documents'; import { configLoader } from '../../lib/documents/configLoader'; import { Logger } from '../../logger'; -import { normalizePath } from '../../utils'; +import { normalizePath, urlToPath } from '../../utils'; import { DocumentSnapshot, SvelteSnapshotOptions } from './DocumentSnapshot'; import { createSvelteModuleLoader } from './module-loader'; import { @@ -13,7 +13,7 @@ import { ignoredBuildDirectories, SnapshotManager } from './SnapshotManager'; -import { ensureRealSvelteFilePath, findTsConfigPath, hasTsExtensions } from './utils'; +import { ensureRealSvelteFilePath, findTsConfigPath, hasTsExtensions, isSubPath } from './utils'; export interface LanguageServiceContainer { readonly tsconfigPath: string; @@ -78,7 +78,19 @@ export async function getService( docContext: LanguageServiceDocumentContext ): Promise { const tsconfigPath = findTsConfigPath(path, workspaceUris, docContext.tsSystem.fileExists); - return getServiceForTsconfig(tsconfigPath, docContext); + + if (tsconfigPath) { + return getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), docContext); + } + + const nearestWorkspaceUri = workspaceUris.find((workspaceUri) => isSubPath(workspaceUri, path)); + + return getServiceForTsconfig( + tsconfigPath, + (nearestWorkspaceUri && urlToPath(nearestWorkspaceUri)) ?? + docContext.tsSystem.getCurrentDirectory(), + docContext + ); } export async function forAllServices( @@ -95,11 +107,14 @@ export async function forAllServices( */ export async function getServiceForTsconfig( tsconfigPath: string, + workspacePath: string, docContext: LanguageServiceDocumentContext ): Promise { + const tsconfigPathOrWorkspacePath = tsconfigPath || workspacePath; + let service: LanguageServiceContainer; - if (services.has(tsconfigPath)) { - service = await services.get(tsconfigPath)!; + if (services.has(tsconfigPathOrWorkspacePath)) { + service = await services.get(tsconfigPathOrWorkspacePath)!; } else { const reloading = pendingReloads.has(tsconfigPath); @@ -110,8 +125,8 @@ export async function getServiceForTsconfig( } pendingReloads.delete(tsconfigPath); - const newService = createLanguageService(tsconfigPath, docContext); - services.set(tsconfigPath, newService); + const newService = createLanguageService(tsconfigPath, workspacePath, docContext); + services.set(tsconfigPathOrWorkspacePath, newService); service = await newService; } @@ -120,9 +135,9 @@ export async function getServiceForTsconfig( async function createLanguageService( tsconfigPath: string, + workspacePath: string, docContext: LanguageServiceDocumentContext ): Promise { - const workspacePath = tsconfigPath ? dirname(tsconfigPath) : ''; const { tsSystem } = docContext; const { @@ -137,7 +152,7 @@ async function createLanguageService( docContext.globalSnapshotsManager, files, raw, - workspacePath || process.cwd() + workspacePath ); // Load all configs within the tsconfig scope and the one above so that they are all loaded @@ -145,7 +160,7 @@ async function createLanguageService( // the default language. await configLoader.loadConfigs(workspacePath); - const svelteModuleLoader = createSvelteModuleLoader(getSnapshot, compilerOptions); + const svelteModuleLoader = createSvelteModuleLoader(getSnapshot, compilerOptions, tsSystem); let svelteTsPath: string; try { @@ -263,7 +278,8 @@ async function createLanguageService( const newSnapshot = DocumentSnapshot.fromFilePath( filePath, docContext.createDocument, - transformationConfig + transformationConfig, + tsSystem ); snapshotManager.set(filePath, newSnapshot); return newSnapshot; @@ -281,7 +297,8 @@ async function createLanguageService( doc = DocumentSnapshot.fromFilePath( fileName, docContext.createDocument, - transformationConfig + transformationConfig, + tsSystem ); snapshotManager.set(fileName, doc); return doc; diff --git a/packages/language-server/src/plugins/typescript/svelte-sys.ts b/packages/language-server/src/plugins/typescript/svelte-sys.ts index 64a3c840d..ff5d8b088 100644 --- a/packages/language-server/src/plugins/typescript/svelte-sys.ts +++ b/packages/language-server/src/plugins/typescript/svelte-sys.ts @@ -5,14 +5,17 @@ import { ensureRealSvelteFilePath, isVirtualSvelteFilePath, toRealSvelteFilePath /** * This should only be accessed by TS svelte module resolution. */ -export function createSvelteSys(getSnapshot: (fileName: string) => DocumentSnapshot) { +export function createSvelteSys( + getSnapshot: (fileName: string) => DocumentSnapshot, + tsSystem: ts.System +) { const fileExistsCache = new Map(); const svelteSys: ts.System & { deleteFromCache: (path: string) => void } = { - ...ts.sys, + ...tsSystem, fileExists(path: string) { path = ensureRealSvelteFilePath(path); - const exists = fileExistsCache.get(path) ?? ts.sys.fileExists(path); + const exists = fileExistsCache.get(path) ?? tsSystem.fileExists(path); fileExistsCache.set(path, exists); return exists; }, @@ -23,19 +26,19 @@ export function createSvelteSys(getSnapshot: (fileName: string) => DocumentSnaps readDirectory(path, extensions, exclude, include, depth) { const extensionsWithSvelte = (extensions ?? []).concat('.svelte'); - return ts.sys.readDirectory(path, extensionsWithSvelte, exclude, include, depth); + return tsSystem.readDirectory(path, extensionsWithSvelte, exclude, include, depth); }, deleteFile(path) { fileExistsCache.delete(ensureRealSvelteFilePath(path)); - return ts.sys.deleteFile?.(path); + return tsSystem.deleteFile?.(path); }, deleteFromCache(path) { fileExistsCache.delete(ensureRealSvelteFilePath(path)); } }; - if (ts.sys.realpath) { - const realpath = ts.sys.realpath; + if (tsSystem.realpath) { + const realpath = tsSystem.realpath; svelteSys.realpath = function (path) { if (isVirtualSvelteFilePath(path)) { return realpath(toRealSvelteFilePath(path)) + '.ts'; diff --git a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts index 09c55fce8..e122711e8 100644 --- a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts @@ -23,6 +23,7 @@ import { LSAndTSDocResolver } from '../../../../src/plugins/typescript/LSAndTSDo import { sortBy } from 'lodash'; import { LSConfigManager } from '../../../../src/ls-config'; import { __resetCache } from '../../../../src/plugins/typescript/service'; +import { getRandomVirtualDirPath, setupVirtualEnvironment } from '../test-utils'; const testDir = join(__dirname, '..'); const testFilesDir = join(testDir, 'testfiles', 'completions'); @@ -1320,6 +1321,52 @@ function test(useNewTransformation: boolean) { ); }); + it('can auto import in workspace without tsconfig/jsconfig', async () => { + const virtualTestDir = getRandomVirtualDirPath(testFilesDir); + const { docManager, document, lsAndTsDocResolver, lsConfigManager, virtualSystem } = + setupVirtualEnvironment({ + filename: 'index.svelte', + fileContent: '', + testDir: virtualTestDir, + useNewTransformation + }); + + const mockPackageDir = join(virtualTestDir, 'node_modules', '@types/random-package'); + + // the main problem is how ts resolve reference type directive + // it would start with a relative url and fail to auto import + virtualSystem.writeFile( + join(mockPackageDir, 'index.d.ts'), + '/// ' + '\nexport function bar(): string' + ); + + virtualSystem.writeFile( + join(virtualTestDir, 'node_modules', '@types', 'random-package2', 'index.d.ts'), + 'declare function foo(): string\n' + 'export = foo' + ); + + const completionProvider = new CompletionsProviderImpl( + lsAndTsDocResolver, + lsConfigManager + ); + + // let the language service aware of random-package and random-package2 + docManager.openDocument({ + text: '', + uri: pathToUrl(join(virtualTestDir, 'test.svelte')) + }); + + const completions = await completionProvider.getCompletions(document, { + line: 0, + character: 9 + }); + const item = completions?.items.find((item) => item.label === 'foo'); + + const { detail } = await completionProvider.resolveCompletion(document, item!); + + assert.strictEqual(detail, 'Auto import from random-package2\nfunction foo(): string'); + }); + // Hacky, but it works. Needed due to testing both new and old transformation after(() => { __resetCache(); diff --git a/packages/language-server/test/plugins/typescript/module-loader.test.ts b/packages/language-server/test/plugins/typescript/module-loader.test.ts index 5ecbbe150..34f41b409 100644 --- a/packages/language-server/test/plugins/typescript/module-loader.test.ts +++ b/packages/language-server/test/plugins/typescript/module-loader.test.ts @@ -24,7 +24,11 @@ describe('createSvelteModuleLoader', () => { sinon.stub(svS, 'createSvelteSys').returns(svelteSys); const compilerOptions: ts.CompilerOptions = { strict: true, paths: { '/@/*': [] } }; - const moduleResolver = createSvelteModuleLoader(getSvelteSnapshotStub, compilerOptions); + const moduleResolver = createSvelteModuleLoader( + getSvelteSnapshotStub, + compilerOptions, + ts.sys + ); return { getSvelteSnapshotStub, diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index b6d20534e..65402456f 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -1,80 +1,20 @@ -import path from 'path'; import assert from 'assert'; -import ts, { FileWatcherEventKind } from 'typescript'; +import path from 'path'; +import ts from 'typescript'; import { Document } from '../../../src/lib/documents'; import { getService, LanguageServiceDocumentContext } from '../../../src/plugins/typescript/service'; import { GlobalSnapshotsManager } from '../../../src/plugins/typescript/SnapshotManager'; -import { normalizePath, pathToUrl } from '../../../src/utils'; +import { pathToUrl } from '../../../src/utils'; +import { createVirtualTsSystem, getRandomVirtualDirPath } from './test-utils'; describe('service', () => { const testDir = path.join(__dirname, 'testfiles'); function setup() { - const virtualFs = new Map(); - // array behave more similar to the actual fs event than Set - const watchers = new Map(); - const watchTimeout = new Map>>(); - - const virtualSystem: ts.System = { - ...ts.sys, - writeFile(path, data) { - const normalizedPath = normalizePath(path); - const existsBefore = virtualFs.has(normalizedPath); - virtualFs.set(normalizedPath, data); - triggerWatch( - normalizedPath, - existsBefore ? ts.FileWatcherEventKind.Changed : ts.FileWatcherEventKind.Created - ); - }, - readFile(path) { - return virtualFs.get(normalizePath(path)); - }, - fileExists(path) { - return virtualFs.has(normalizePath(path)); - }, - deleteFile(path) { - const normalizedPath = normalizePath(path); - const existsBefore = virtualFs.has(normalizedPath); - virtualFs.delete(normalizedPath); - - if (existsBefore) { - triggerWatch(normalizedPath, ts.FileWatcherEventKind.Deleted); - } - }, - watchFile(path, callback) { - const normalizedPath = normalizePath(path); - let watchersOfPath = watchers.get(normalizedPath); - - if (!watchersOfPath) { - watchersOfPath = []; - watchers.set(normalizedPath, watchersOfPath); - } - - watchersOfPath.push(callback); - - return { - close() { - const watchersOfPath = watchers.get(normalizedPath); - - if (watchersOfPath) { - watchers.set( - normalizedPath, - watchersOfPath.filter((watcher) => watcher === callback) - ); - } - - const timeouts = watchTimeout.get(normalizedPath); - - if (timeouts != null) { - timeouts.forEach((timeout) => clearTimeout(timeout)); - } - } - }; - } - }; + const virtualSystem = createVirtualTsSystem(testDir); const lsDocumentContext: LanguageServiceDocumentContext = { ambientTypesSource: 'svelte2tsx', @@ -82,7 +22,7 @@ describe('service', () => { return new Document(pathToUrl(fileName), content); }, extendedConfigCache: new Map(), - globalSnapshotsManager: new GlobalSnapshotsManager(), + globalSnapshotsManager: new GlobalSnapshotsManager(virtualSystem), transformOnTemplateError: true, tsSystem: virtualSystem, useNewTransformation: true, @@ -94,33 +34,10 @@ describe('service', () => { const rootUris = [pathToUrl(testDir)]; return { virtualSystem, lsDocumentContext, rootUris }; - - function triggerWatch(normalizedPath: string, kind: FileWatcherEventKind) { - let timeoutsOfPath = watchTimeout.get(normalizedPath); - - if (!timeoutsOfPath) { - timeoutsOfPath = []; - watchTimeout.set(normalizedPath, timeoutsOfPath); - } - - timeoutsOfPath.push( - setTimeout( - () => - watchers - .get(normalizedPath) - ?.forEach((callback) => callback(normalizedPath, kind)), - 0 - ) - ); - } - } - - function getRandomVirtualDirPath() { - return path.join(testDir, `virtual-path-${Math.floor(Math.random() * 100_000)}`); } it('can find tsconfig and override with default config', async () => { - const dirPath = getRandomVirtualDirPath(); + const dirPath = getRandomVirtualDirPath(testDir); const { virtualSystem, lsDocumentContext, rootUris } = setup(); virtualSystem.writeFile( @@ -181,7 +98,7 @@ describe('service', () => { } it('can watch tsconfig', async () => { - const dirPath = getRandomVirtualDirPath(); + const dirPath = getRandomVirtualDirPath(testDir); const { virtualSystem, lsDocumentContext, rootUris } = setup(); const tsconfigPath = path.join(dirPath, 'tsconfig.json'); @@ -226,7 +143,7 @@ describe('service', () => { }); it('can watch extended tsconfig', async () => { - const dirPath = getRandomVirtualDirPath(); + const dirPath = getRandomVirtualDirPath(testDir); const { virtualSystem, lsDocumentContext, rootUris } = setup(); const tsconfigPath = path.join(dirPath, 'tsconfig.json'); const extend = './.svelte-kit/tsconfig.json'; diff --git a/packages/language-server/test/plugins/typescript/svelte-sys.test.ts b/packages/language-server/test/plugins/typescript/svelte-sys.test.ts index f74c08882..3d0287c57 100644 --- a/packages/language-server/test/plugins/typescript/svelte-sys.test.ts +++ b/packages/language-server/test/plugins/typescript/svelte-sys.test.ts @@ -23,8 +23,11 @@ describe('Svelte Sys', () => { } ); - sinon.replace(ts.sys, 'fileExists', fileExistsStub); - const loader = createSvelteSys(getSnapshotStub); + // sinon.replace(ts.sys, 'fileExists', fileExistsStub); + const loader = createSvelteSys(getSnapshotStub, { + ...ts.sys, + fileExists: fileExistsStub + }); return { tsFile, diff --git a/packages/language-server/test/plugins/typescript/test-utils.ts b/packages/language-server/test/plugins/typescript/test-utils.ts new file mode 100644 index 000000000..9672deb14 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/test-utils.ts @@ -0,0 +1,155 @@ +import path, { isAbsolute, join } from 'path'; +import ts from 'typescript'; +import { DocumentManager, Document } from '../../../src/lib/documents'; +import { LSConfigManager } from '../../../src/ls-config'; +import { LSAndTSDocResolver } from '../../../src/plugins'; +import { normalizePath, pathToUrl } from '../../../src/utils'; + +export function createVirtualTsSystem(currentDirectory: string): ts.System { + const virtualFs = new Map(); + // array behave more similar to the actual fs event than Set + const watchers = new Map(); + const watchTimeout = new Map>>(); + + function toAbsolute(path: string) { + return isAbsolute(path) ? path : join(currentDirectory, path); + } + + const virtualSystem: ts.System = { + ...ts.sys, + getCurrentDirectory() { + return currentDirectory; + }, + writeFile(path, data) { + const normalizedPath = normalizePath(toAbsolute(path)); + const existsBefore = virtualFs.has(normalizedPath); + virtualFs.set(normalizedPath, data); + triggerWatch( + normalizedPath, + existsBefore ? ts.FileWatcherEventKind.Changed : ts.FileWatcherEventKind.Created + ); + }, + readFile(path) { + return virtualFs.get(normalizePath(toAbsolute(path))); + }, + fileExists(path) { + return virtualFs.has(normalizePath(toAbsolute(path))); + }, + directoryExists(path) { + const normalizedPath = normalizePath(toAbsolute(path)); + return Array.from(virtualFs.keys()).some((fileName) => + fileName.startsWith(normalizedPath) + ); + }, + deleteFile(path) { + const normalizedPath = normalizePath(toAbsolute(path)); + const existsBefore = virtualFs.has(normalizedPath); + virtualFs.delete(normalizedPath); + + if (existsBefore) { + triggerWatch(normalizedPath, ts.FileWatcherEventKind.Deleted); + } + }, + watchFile(path, callback) { + const normalizedPath = normalizePath(toAbsolute(path)); + let watchersOfPath = watchers.get(normalizedPath); + + if (!watchersOfPath) { + watchersOfPath = []; + watchers.set(normalizedPath, watchersOfPath); + } + + watchersOfPath.push(callback); + + return { + close() { + const watchersOfPath = watchers.get(normalizedPath); + + if (watchersOfPath) { + watchers.set( + normalizedPath, + watchersOfPath.filter((watcher) => watcher === callback) + ); + } + + const timeouts = watchTimeout.get(normalizedPath); + + if (timeouts != null) { + timeouts.forEach((timeout) => clearTimeout(timeout)); + } + } + }; + } + }; + + return virtualSystem; + + function triggerWatch(normalizedPath: string, kind: ts.FileWatcherEventKind) { + let timeoutsOfPath = watchTimeout.get(normalizedPath); + + if (!timeoutsOfPath) { + timeoutsOfPath = []; + watchTimeout.set(normalizedPath, timeoutsOfPath); + } + + timeoutsOfPath.push( + setTimeout( + () => + watchers + .get(normalizedPath) + ?.forEach((callback) => callback(normalizedPath, kind)), + 0 + ) + ); + } +} + +export function getRandomVirtualDirPath(testDir: string) { + return path.join(testDir, `virtual-path-${Math.floor(Math.random() * 100_000)}`); +} + +interface VirtualEnvironmentOptions { + testDir: string; + filename: string; + useNewTransformation: boolean; + fileContent: string; +} + +export function setupVirtualEnvironment({ + testDir, + fileContent, + filename, + useNewTransformation +}: VirtualEnvironmentOptions) { + const docManager = new DocumentManager( + (textDocument) => new Document(textDocument.uri, textDocument.text) + ); + + const lsConfigManager = new LSConfigManager(); + lsConfigManager.update({ svelte: { useNewTransformation } }); + + const virtualSystem = createVirtualTsSystem(testDir); + const lsAndTsDocResolver = new LSAndTSDocResolver( + docManager, + [pathToUrl(testDir)], + lsConfigManager, + { + tsSystem: virtualSystem + } + ); + + const filePath = join(testDir, filename); + virtualSystem.writeFile(filePath, fileContent); + const document = docManager.openDocument({ + uri: pathToUrl(filePath), + text: virtualSystem.readFile(filePath) || '' + }); + + return { + lsAndTsDocResolver, + document, + docManager, + virtualSystem, + lsConfigManager + }; +}