From 541738f7b5da4785c2a6bf037b708d4e2e2bff1f Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Mon, 22 Jan 2024 18:10:45 +0800 Subject: [PATCH 01/36] resolve module with the compilerOptions from project reference --- .../src/plugins/typescript/module-loader.ts | 12 ++-- .../src/plugins/typescript/service.ts | 7 ++- .../src/plugins/typescript/serviceCache.ts | 4 +- .../test/plugins/typescript/service.test.ts | 63 ++++++++++++++++++- .../project-reference/paths/imported.ts | 1 + .../project-reference/paths/importing.svelte | 4 ++ .../project-reference/paths/tsconfig_sub.json | 9 +++ .../services/project-reference/tsconfig.json | 11 ++++ 8 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts create mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte create mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json create mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index 4eefdc700..e9876e731 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -216,7 +216,7 @@ export function createSvelteModuleLoader( moduleNames: string[], containingFile: string, _reusedNames: string[] | undefined, - _redirectedReference: ts.ResolvedProjectReference | undefined, + redirectedReference: ts.ResolvedProjectReference | undefined, _options: ts.CompilerOptions, containingSourceFile?: ts.SourceFile | undefined ): Array { @@ -229,7 +229,8 @@ export function createSvelteModuleLoader( moduleName, containingFile, containingSourceFile, - index + index, + redirectedReference ); resolvedModule?.failedLookupLocations?.forEach((failedLocation) => { @@ -247,7 +248,8 @@ export function createSvelteModuleLoader( name: string, containingFile: string, containingSourceFile: ts.SourceFile | undefined, - index: number + index: number, + redirectedReference: ts.ResolvedProjectReference | undefined ): ts.ResolvedModuleWithFailedLookupLocations { const mode = impliedNodeFormatResolver.resolve( name, @@ -264,7 +266,7 @@ export function createSvelteModuleLoader( compilerOptions, ts.sys, tsModuleCache, - undefined, + redirectedReference, mode ); @@ -279,7 +281,7 @@ export function createSvelteModuleLoader( compilerOptions, svelteSys, undefined, - undefined, + redirectedReference, mode ); diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 5f2571e75..d40b60818 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -205,6 +205,7 @@ async function createLanguageService( options: compilerOptions, errors: configErrors, fileNames: files, + projectReferences, raw, extendedConfigPaths } = getParsedConfig(); @@ -276,6 +277,7 @@ async function createLanguageService( resolveModuleNames: svelteModuleLoader.resolveModuleNames, readDirectory: svelteModuleLoader.readDirectory, getDirectories: tsSystem.getDirectories, + getProjectReferences: () => projectReferences, useCaseSensitiveFileNames: () => tsSystem.useCaseSensitiveFileNames, getScriptKind: (fileName: string) => getSnapshot(fileName).scriptKind, getProjectVersion: () => projectVersion.toString(), @@ -283,7 +285,10 @@ async function createLanguageService( resolveTypeReferenceDirectiveReferences: svelteModuleLoader.resolveTypeReferenceDirectiveReferences, hasInvalidatedResolutions: svelteModuleLoader.mightHaveInvalidatedResolutions, - getModuleResolutionCache: svelteModuleLoader.getModuleResolutionCache + getModuleResolutionCache: svelteModuleLoader.getModuleResolutionCache, + useSourceOfProjectReferenceRedirect() { + return !languageServiceReducedMode; + }, }; const documentRegistry = getOrCreateDocumentRegistry( diff --git a/packages/language-server/src/plugins/typescript/serviceCache.ts b/packages/language-server/src/plugins/typescript/serviceCache.ts index 0dbb3bfe2..2a6b34860 100644 --- a/packages/language-server/src/plugins/typescript/serviceCache.ts +++ b/packages/language-server/src/plugins/typescript/serviceCache.ts @@ -81,7 +81,9 @@ export function createProject( 'getPackageJsonsVisibleToFile', 'getPackageJsonAutoImportProvider', 'includePackageJsonAutoImports', - 'useSourceOfProjectReferenceRedirect' + // Volar doesn't have the "languageServiceReducedMode" support but we do + // so don't proxy this method and use our own implementation + // 'useSourceOfProjectReferenceRedirect' ]; proxyMethods.forEach((key) => ((host as any)[key] = project[key].bind(project))); diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index f8b36914a..78a484e7b 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -1,7 +1,7 @@ import assert from 'assert'; import path from 'path'; import ts from 'typescript'; -import { Document } from '../../../src/lib/documents'; +import { Document, DocumentManager } from '../../../src/lib/documents'; import { getService, LanguageServiceDocumentContext @@ -9,9 +9,13 @@ import { import { GlobalSnapshotsManager } from '../../../src/plugins/typescript/SnapshotManager'; import { pathToUrl } from '../../../src/utils'; import { createVirtualTsSystem, getRandomVirtualDirPath } from './test-utils'; +import { createProjectService } from '../../../src/plugins/typescript/serviceCache'; +import { LSConfigManager } from '../../../src/ls-config'; +import { LSAndTSDocResolver } from '../../../src/plugins'; describe('service', () => { const testDir = path.join(__dirname, 'testfiles'); + const serviceTestDir = path.join(testDir, 'services'); function setup() { const virtualSystem = createVirtualTsSystem(testDir); @@ -262,4 +266,61 @@ describe('service', () => { ls.getService().getSemanticDiagnostics(document.getFilePath()!); }); }); + + it('resolve module with the compilerOptions from project reference', async () => { + // Don't test this with module not found diagnostics + // because there is a case where is won't have an error but still not resolved the module + + const { lsAndTsDocResolver, docManager } = setupRealFS(); + const pathsProjectRefDir = path.join(serviceTestDir, 'project-reference', 'paths'); + + const importingFilePath = path.join(pathsProjectRefDir, 'importing.svelte'); + const document = docManager.openClientDocument({ + uri: pathToUrl(importingFilePath), + text: ts.sys.readFile(importingFilePath) || '' + }); + + const lsContainer = await lsAndTsDocResolver.getTSService(importingFilePath); + lsContainer.getService(); + const snapshotManager = lsContainer.snapshotManager; + + const importedFilePath = path.join(pathsProjectRefDir, 'imported.ts'); + assert.equal( + snapshotManager.get(importedFilePath), + undefined, + 'expected to load imported file through module resolution' + ); + + // uncomment the import + document.update( + '', + document.offsetAt({ + line: 2, + character: 0 + }), + document.offsetAt({ + line: 2, + character: 2 + }) + ); + lsContainer.updateSnapshot(document); + lsContainer.getService(); + + assert.ok(snapshotManager.get(importedFilePath)); + }); + + function setupRealFS() { + const docManager = new DocumentManager( + (textDocument) => new Document(textDocument.uri, textDocument.text) + ); + const lsConfigManager = new LSConfigManager(); + const workspaceUris = [pathToUrl(serviceTestDir)]; + const lsAndTsDocResolver = new LSAndTSDocResolver( + docManager, + workspaceUris, + lsConfigManager + ); + + return { lsAndTsDocResolver, docManager }; + } }); diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts new file mode 100644 index 000000000..4684b3c04 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts @@ -0,0 +1 @@ +export const hi = 1; \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte new file mode 100644 index 000000000..8667b3a89 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json new file mode 100644 index 000000000..a7307a5a6 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "composite": true, + "paths": { + "hi": ["./imported.ts"] + } + } +} \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json new file mode 100644 index 000000000..83937370e --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": [], + "references": [ + { + "path": "./paths/tsconfig_sub.json" + } + ], + "compilerOptions": { + "types": [] + } +} From 124ea40ed73021db2eb2ac53798ff36da05fcb8c Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 24 Jan 2024 09:22:05 +0800 Subject: [PATCH 02/36] fix test, might already be loaded in global snapshot manager --- .../test/plugins/typescript/service.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index 78a484e7b..296d1c189 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -9,7 +9,6 @@ import { import { GlobalSnapshotsManager } from '../../../src/plugins/typescript/SnapshotManager'; import { pathToUrl } from '../../../src/utils'; import { createVirtualTsSystem, getRandomVirtualDirPath } from './test-utils'; -import { createProjectService } from '../../../src/plugins/typescript/serviceCache'; import { LSConfigManager } from '../../../src/ls-config'; import { LSAndTSDocResolver } from '../../../src/plugins'; @@ -286,8 +285,8 @@ describe('service', () => { const importedFilePath = path.join(pathsProjectRefDir, 'imported.ts'); assert.equal( - snapshotManager.get(importedFilePath), - undefined, + snapshotManager.has(importedFilePath), + false, 'expected to load imported file through module resolution' ); @@ -306,7 +305,7 @@ describe('service', () => { lsContainer.updateSnapshot(document); lsContainer.getService(); - assert.ok(snapshotManager.get(importedFilePath)); + assert.ok(snapshotManager.has(importedFilePath)); }); function setupRealFS() { From 56d1959f23f85b8c3fa8563324f9618004acc791 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 6 Mar 2024 09:16:31 +0800 Subject: [PATCH 03/36] wip --- .../src/lib/documents/DocumentManager.ts | 3 +- .../src/plugins/typescript/module-loader.ts | 49 +++--- .../src/plugins/typescript/service.ts | 27 +++- .../src/plugins/typescript/svelte-sys.ts | 8 +- .../project-reference.only/paths/imported.ts | 1 + .../project-reference.only/paths/input.svelte | 5 + .../paths/tsconfig_sub.json | 1 + .../project-reference.only}/tsconfig.json | 0 .../diagnostics/fixtures/tsconfig.json | 2 +- .../plugins/typescript/module-loader.test.ts | 145 ++---------------- .../project-reference/paths/imported.ts | 1 - .../project-reference/paths/importing.svelte | 4 - 12 files changed, 77 insertions(+), 169 deletions(-) create mode 100644 packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/imported.ts create mode 100644 packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/input.svelte rename packages/language-server/test/plugins/typescript/{testfiles/services/project-reference => features/diagnostics/fixtures/project-reference.only}/paths/tsconfig_sub.json (86%) rename packages/language-server/test/plugins/typescript/{testfiles/services/project-reference => features/diagnostics/fixtures/project-reference.only}/tsconfig.json (100%) delete mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts delete mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte diff --git a/packages/language-server/src/lib/documents/DocumentManager.ts b/packages/language-server/src/lib/documents/DocumentManager.ts index bbf86303b..dafeac78a 100644 --- a/packages/language-server/src/lib/documents/DocumentManager.ts +++ b/packages/language-server/src/lib/documents/DocumentManager.ts @@ -48,14 +48,15 @@ export class DocumentManager { if (this.documents.has(textDocument.uri)) { document = this.documents.get(textDocument.uri)!; document.setText(textDocument.text); + document.openedByClient = openedByClient; } else { document = this.createDocument(textDocument); this.documents.set(textDocument.uri, document); + document.openedByClient = openedByClient; this.notify('documentOpen', document); } this.notify('documentChange', document); - document.openedByClient = openedByClient; return document; } diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index e9876e731..a783bd1af 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -161,7 +161,8 @@ export function createSvelteModuleLoader( getSnapshot: (fileName: string) => DocumentSnapshot, compilerOptions: ts.CompilerOptions, tsSystem: ts.System, - tsModule: typeof ts + tsModule: typeof ts, + getModuleResolutionHost: () => ts.ModuleResolutionHost | undefined ) { const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); const svelteSys = createSvelteSys(tsSystem); @@ -201,9 +202,16 @@ export function createSvelteModuleLoader( const previousTriedButFailed = failedPathToContainingFile.get(path); - for (const containingFile of previousTriedButFailed ?? []) { + if (!previousTriedButFailed) { + return; + } + + for (const containingFile of previousTriedButFailed) { failedLocationInvalidated.add(containingFile); } + + tsModuleCache.clear(); + typeReferenceCache.clear(); }, resolveModuleNames, resolveTypeReferenceDirectiveReferences, @@ -257,52 +265,39 @@ export function createSvelteModuleLoader( containingSourceFile, compilerOptions ); - // Delegate to the TS resolver first. - // If that does not bring up anything, try the Svelte Module loader - // which is able to deal with .svelte files. - const tsResolvedModuleWithFailedLookup = tsModule.resolveModuleName( + const resolvedModuleWithFailedLookup = tsModule.resolveModuleName( name, containingFile, compilerOptions, - ts.sys, + getModuleResolutionHost() ?? svelteSys, tsModuleCache, redirectedReference, mode ); - const tsResolvedModule = tsResolvedModuleWithFailedLookup.resolvedModule; - if (tsResolvedModule) { - return tsResolvedModuleWithFailedLookup; + const resolvedModule = resolvedModuleWithFailedLookup.resolvedModule; + + if (!resolvedModule || !isVirtualSvelteFilePath(resolvedModule.resolvedFileName)) { + return resolvedModuleWithFailedLookup; } - const svelteResolvedModuleWithFailedLookup = tsModule.resolveModuleName( - name, - containingFile, - compilerOptions, - svelteSys, - undefined, - redirectedReference, - mode + const resolvedFileName = svelteSys.getRealSveltePathIfExists( + resolvedModule.resolvedFileName ); - const svelteResolvedModule = svelteResolvedModuleWithFailedLookup.resolvedModule; - if ( - !svelteResolvedModule || - !isVirtualSvelteFilePath(svelteResolvedModule.resolvedFileName) - ) { - return svelteResolvedModuleWithFailedLookup; + if (!isSvelteFilePath(resolvedFileName)) { + return resolvedModuleWithFailedLookup; } - const resolvedFileName = ensureRealSvelteFilePath(svelteResolvedModule.resolvedFileName); const snapshot = getSnapshot(resolvedFileName); const resolvedSvelteModule: ts.ResolvedModuleFull = { extension: getExtensionFromScriptKind(snapshot && snapshot.scriptKind), resolvedFileName, - isExternalLibraryImport: svelteResolvedModule.isExternalLibraryImport + isExternalLibraryImport: resolvedModule.isExternalLibraryImport }; return { - ...svelteResolvedModuleWithFailedLookup, + ...resolvedModuleWithFailedLookup, resolvedModule: resolvedSvelteModule }; } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index d40b60818..3d9e47b4e 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -1,4 +1,4 @@ -import { basename, dirname, join, resolve } from 'path'; +import { dirname, join, resolve } from 'path'; import ts from 'typescript'; import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; import { getPackageInfo, importSvelte } from '../../importPackage'; @@ -49,6 +49,7 @@ export interface LanguageServiceContainer { fileBelongsToProject(filePath: string, isNew: boolean): boolean; onAutoImportProviderSettingsChanged(): void; onPackageJsonChange(packageJsonPath: string): void; + scheduleUpdate(triggeredFile?: string): void; dispose(): void; } @@ -62,7 +63,12 @@ declare module 'typescript' { */ hasInvalidatedResolutions?: (sourceFile: string) => boolean; + /** + * @internal + */ getModuleResolutionCache?(): ts.ModuleResolutionCache; + /** @internal */ + setCompilerHost?(host: ts.CompilerHost): void; } interface ResolvedModuleWithFailedLookupLocations { @@ -222,9 +228,16 @@ async function createLanguageService( // by the time they need to be accessed synchronously by DocumentSnapshots. await configLoader.loadConfigs(workspacePath); - const svelteModuleLoader = createSvelteModuleLoader(getSnapshot, compilerOptions, tsSystem, ts); + const svelteModuleLoader = createSvelteModuleLoader( + getSnapshot, + compilerOptions, + tsSystem, + ts, + () => host?.getCompilerHost?.() + ); let svelteTsPath: string; + let compilerHost: ts.CompilerHost | undefined; try { // For when svelte2tsx/svelte-check is part of node_modules, for example VS Code extension svelteTsPath = dirname(require.resolve(docContext.ambientTypesSource)); @@ -276,6 +289,7 @@ async function createLanguageService( readFile: svelteModuleLoader.readFile, resolveModuleNames: svelteModuleLoader.resolveModuleNames, readDirectory: svelteModuleLoader.readDirectory, + realpath: tsSystem.realpath, getDirectories: tsSystem.getDirectories, getProjectReferences: () => projectReferences, useCaseSensitiveFileNames: () => tsSystem.useCaseSensitiveFileNames, @@ -288,7 +302,9 @@ async function createLanguageService( getModuleResolutionCache: svelteModuleLoader.getModuleResolutionCache, useSourceOfProjectReferenceRedirect() { return !languageServiceReducedMode; - }, + } + // setCompilerHost: (host) => (compilerHost = host), + // getCompilerHost: () => compilerHost, }; const documentRegistry = getOrCreateDocumentRegistry( @@ -327,6 +343,7 @@ async function createLanguageService( invalidateModuleCache, onAutoImportProviderSettingsChanged, onPackageJsonChange, + scheduleUpdate, dispose }; @@ -360,6 +377,9 @@ async function createLanguageService( function updateSnapshotFromDocument(document: Document): DocumentSnapshot { const filePath = document.getFilePath() || ''; const prevSnapshot = snapshotManager.get(filePath); + + console.log(filePath, prevSnapshot?.isOpenedInClient(), document.openedByClient) + if (prevSnapshot?.version === document.version) { return prevSnapshot; } @@ -723,6 +743,7 @@ async function createLanguageService( } dirty = false; + compilerHost = undefined; if (!oldProgram) { changedFilesForExportCache.clear(); diff --git a/packages/language-server/src/plugins/typescript/svelte-sys.ts b/packages/language-server/src/plugins/typescript/svelte-sys.ts index 4fb3c205c..a639b2a58 100644 --- a/packages/language-server/src/plugins/typescript/svelte-sys.ts +++ b/packages/language-server/src/plugins/typescript/svelte-sys.ts @@ -20,12 +20,18 @@ export function createSvelteSys(tsSystem: ts.System) { } } + function getRealSveltePathIfExists(path: string) { + return svelteFileExists(path) ? toRealSvelteFilePath(path) : path; + } + const svelteSys: ts.System & { deleteFromCache: (path: string) => void; svelteFileExists: (path: string) => boolean; + getRealSveltePathIfExists: (path: string) => string; } = { ...tsSystem, svelteFileExists, + getRealSveltePathIfExists, fileExists(path: string) { // We need to check both .svelte and .svelte.ts/js because that's how Svelte 5 will likely mark files with runes in them const sveltePathExists = svelteFileExists(path); @@ -36,7 +42,7 @@ export function createSvelteSys(tsSystem: ts.System) { }, readFile(path: string) { // No getSnapshot here, because TS will very rarely call this and only for files that are not in the project - return tsSystem.readFile(svelteFileExists(path) ? toRealSvelteFilePath(path) : path); + return tsSystem.readFile(getRealSveltePathIfExists(path)); }, readDirectory(path, extensions, exclude, include, depth) { const extensionsWithSvelte = extensions ? [...extensions, '.svelte'] : undefined; diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/imported.ts b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/imported.ts new file mode 100644 index 000000000..2a9042acc --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/imported.ts @@ -0,0 +1 @@ +export function hi(cb: (num: number) => string) {}; \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/input.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/input.svelte new file mode 100644 index 000000000..fb6f5dfef --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/tsconfig_sub.json similarity index 86% rename from packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json rename to packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/tsconfig_sub.json index a7307a5a6..e496c36b6 100644 --- a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/tsconfig_sub.json @@ -2,6 +2,7 @@ "compilerOptions": { "baseUrl": ".", "composite": true, + "strict": true, "paths": { "hi": ["./imported.ts"] } diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/tsconfig.json similarity index 100% rename from packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json rename to packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/tsconfig.json diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json index f8add4155..dd94fecae 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json @@ -7,5 +7,5 @@ */ "types": ["svelte"] }, - "exclude": ["./svelte-native/**/*", "./node16/**/*"] + "exclude": ["./svelte-native/**/*", "./node16/**/*", "project-reference/**/*"] } 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 d08e42682..6f84335c8 100644 --- a/packages/language-server/test/plugins/typescript/module-loader.test.ts +++ b/packages/language-server/test/plugins/typescript/module-loader.test.ts @@ -21,8 +21,12 @@ describe('createSvelteModuleLoader', () => { const moduleCacheMock = { getPackageJsonInfoCache: () => ({}) }; + const moduleResolutionHost = { ...ts.sys }; - const svelteSys = 'svelteSys'; + + const svelteSys = { + ...svS.createSvelteSys(ts.sys), + }; sinon.stub(svS, 'createSvelteSys').returns(svelteSys); const compilerOptions: ts.CompilerOptions = { strict: true, paths: { '/@/*': [] } }; @@ -34,7 +38,8 @@ describe('createSvelteModuleLoader', () => { ...ts, createModuleResolutionCache: () => moduleCacheMock, resolveModuleName: resolveStub - } + }, + () => moduleResolutionHost ); return { @@ -43,7 +48,8 @@ describe('createSvelteModuleLoader', () => { resolveStub, compilerOptions, moduleResolver, - svelteSys + svelteSys, + moduleResolutionHost }; } @@ -51,132 +57,18 @@ describe('createSvelteModuleLoader', () => { return stub.getCall(stub.getCalls().length - 1); } - it('uses tsSys for normal files', async () => { - const resolvedModule: ts.ResolvedModuleFull = { - extension: ts.Extension.Ts, - resolvedFileName: 'filename.ts' - }; - const { resolveStub, moduleResolver, compilerOptions, moduleCacheMock } = - setup(resolvedModule); - const result = moduleResolver.resolveModuleNames( - ['./normal.ts'], - 'C:/somerepo/somefile.svelte', - undefined, - undefined, - undefined as any - ); - - assert.deepStrictEqual(result, [resolvedModule]); - assert.deepStrictEqual(lastCall(resolveStub).args, [ - './normal.ts', - 'C:/somerepo/somefile.svelte', - compilerOptions, - ts.sys, - moduleCacheMock, - undefined, - undefined - ]); - }); - - it('uses tsSys for normal files part of TS aliases', async () => { - const resolvedModule: ts.ResolvedModuleFull = { - extension: ts.Extension.Ts, - resolvedFileName: 'filename.ts' - }; - const { resolveStub, moduleResolver, compilerOptions, moduleCacheMock } = - setup(resolvedModule); - const result = moduleResolver.resolveModuleNames( - ['/@/normal'], - 'C:/somerepo/somefile.svelte', - undefined, - undefined, - undefined as any - ); - - assert.deepStrictEqual(result, [resolvedModule]); - assert.deepStrictEqual(lastCall(resolveStub).args, [ - '/@/normal', - 'C:/somerepo/somefile.svelte', - compilerOptions, - ts.sys, - moduleCacheMock, - undefined, - undefined - ]); - }); - - it('uses tsSys for svelte.d.ts files', async () => { - const resolvedModule: ts.ResolvedModuleFull = { - extension: ts.Extension.Dts, - resolvedFileName: 'filename.d.ts' - }; - const { resolveStub, moduleResolver, compilerOptions, moduleCacheMock } = - setup(resolvedModule); - const result = moduleResolver.resolveModuleNames( - ['./normal.ts'], - 'C:/somerepo/somefile.svelte', - undefined, - undefined, - undefined as any - ); - - assert.deepStrictEqual(result, [resolvedModule]); - assert.deepStrictEqual(lastCall(resolveStub).args, [ - './normal.ts', - 'C:/somerepo/somefile.svelte', - compilerOptions, - ts.sys, - moduleCacheMock, - undefined, - undefined - ]); - }); - - it('uses svelte module loader for virtual svelte files', async () => { + it('uses svelte script kind if resolved module is svelte file', async () => { const resolvedModule: ts.ResolvedModuleFull = { extension: ts.Extension.Ts, resolvedFileName: 'filename.svelte.ts' }; - const { resolveStub, svelteSys, moduleResolver, compilerOptions, getSvelteSnapshotStub } = - setup(resolvedModule); - resolveStub.onFirstCall().returns({ resolvedModule: undefined }); - const result = moduleResolver.resolveModuleNames( - ['./svelte.svelte'], - 'C:/somerepo/somefile.svelte', - undefined, - undefined, - undefined as any - ); + const { getSvelteSnapshotStub, moduleResolver, svelteSys } = setup(resolvedModule); - assert.deepStrictEqual(result, [ - { - extension: ts.Extension.Jsx, - resolvedFileName: 'filename.svelte', - isExternalLibraryImport: undefined - } - ]); - assert.deepStrictEqual(lastCall(resolveStub).args, [ - './svelte.svelte', - 'C:/somerepo/somefile.svelte', - compilerOptions, - svelteSys, - undefined, - undefined, - undefined - ]); - assert.deepStrictEqual(lastCall(getSvelteSnapshotStub).args, ['filename.svelte']); - }); + svelteSys.getRealSveltePathIfExists = (filename: string) => + filename === 'filename.svelte.ts' ? 'filename.svelte' : filename; - it('uses svelte module loader for virtual svelte files with TS path aliases', async () => { - const resolvedModule: ts.ResolvedModuleFull = { - extension: ts.Extension.Ts, - resolvedFileName: 'filename.svelte.ts' - }; - const { resolveStub, svelteSys, moduleResolver, compilerOptions, getSvelteSnapshotStub } = - setup(resolvedModule); - resolveStub.onFirstCall().returns({ resolvedModule: undefined }); const result = moduleResolver.resolveModuleNames( - ['/@/svelte.svelte'], + ['./normal.ts'], 'C:/somerepo/somefile.svelte', undefined, undefined, @@ -190,15 +82,6 @@ describe('createSvelteModuleLoader', () => { isExternalLibraryImport: undefined } ]); - assert.deepStrictEqual(lastCall(resolveStub).args, [ - '/@/svelte.svelte', - 'C:/somerepo/somefile.svelte', - compilerOptions, - svelteSys, - undefined, - undefined, - undefined - ]); assert.deepStrictEqual(lastCall(getSvelteSnapshotStub).args, ['filename.svelte']); }); diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts deleted file mode 100644 index 4684b3c04..000000000 --- a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts +++ /dev/null @@ -1 +0,0 @@ -export const hi = 1; \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte deleted file mode 100644 index 8667b3a89..000000000 --- a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte +++ /dev/null @@ -1,4 +0,0 @@ - From d01b67812c4cba04eb6272a2ab65f81289a1ca3a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 31 Jul 2024 09:06:27 +0200 Subject: [PATCH 04/36] breaking: make TypeScript a peer dependency --- packages/svelte-check/package.json | 7 +++--- pnpm-lock.yaml | 37 ++++-------------------------- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/packages/svelte-check/package.json b/packages/svelte-check/package.json index 9d8309c46..3719a6efb 100644 --- a/packages/svelte-check/package.json +++ b/packages/svelte-check/package.json @@ -27,11 +27,11 @@ "chokidar": "^3.4.1", "picocolors": "^1.0.0", "sade": "^1.7.4", - "svelte-preprocess": "^5.1.3", - "typescript": "^5.0.3" + "svelte-preprocess": "^5.1.3" }, "peerDependencies": { - "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" + "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" }, "scripts": { "build": "rollup -c && node ./dist/src/index.js --workspace ./test --tsconfig ./tsconfig.json", @@ -51,6 +51,7 @@ "rollup-plugin-cleanup": "^3.2.0", "rollup-plugin-copy": "^3.4.0", "svelte-language-server": "workspace:*", + "typescript": "^5.5.2", "vscode-languageserver": "8.0.2", "vscode-languageserver-protocol": "3.17.2", "vscode-languageserver-types": "3.17.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7411c3b4d..ca1fb581b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,10 +132,7 @@ importers: version: 3.57.0 svelte-preprocess: specifier: ^5.1.3 - version: 5.1.3(svelte@3.57.0)(typescript@5.4.5) - typescript: - specifier: ^5.0.3 - version: 5.4.5 + version: 5.1.3(svelte@3.57.0)(typescript@5.5.2) devDependencies: '@rollup/plugin-commonjs': specifier: ^24.0.0 @@ -151,7 +148,7 @@ importers: version: 5.0.2(rollup@3.7.5) '@rollup/plugin-typescript': specifier: ^10.0.0 - version: 10.0.1(rollup@3.7.5)(tslib@2.5.2)(typescript@5.4.5) + version: 10.0.1(rollup@3.7.5)(tslib@2.5.2)(typescript@5.5.2) '@types/sade': specifier: ^1.7.2 version: 1.7.4 @@ -173,6 +170,9 @@ importers: svelte-language-server: specifier: workspace:* version: link:../language-server + typescript: + specifier: ^5.5.2 + version: 5.5.2 vscode-languageserver: specifier: 8.0.2 version: 8.0.2 @@ -1295,11 +1295,6 @@ packages: typescript-auto-import-cache@0.3.3: resolution: {integrity: sha512-ojEC7+Ci1ij9eE6hp8Jl9VUNnsEKzztktP5gtYNRMrTmfXVwA1PITYYAkpxCvvupdSYa/Re51B6KMcv1CTZEUA==} - typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} - engines: {node: '>=14.17'} - hasBin: true - typescript@5.5.2: resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} engines: {node: '>=14.17'} @@ -1500,15 +1495,6 @@ snapshots: optionalDependencies: rollup: 3.7.5 - '@rollup/plugin-typescript@10.0.1(rollup@3.7.5)(tslib@2.5.2)(typescript@5.4.5)': - dependencies: - '@rollup/pluginutils': 5.0.2(rollup@3.7.5) - resolve: 1.22.2 - typescript: 5.4.5 - optionalDependencies: - rollup: 3.7.5 - tslib: 2.5.2 - '@rollup/plugin-typescript@10.0.1(rollup@3.7.5)(tslib@2.5.2)(typescript@5.5.2)': dependencies: '@rollup/pluginutils': 5.0.2(rollup@3.7.5) @@ -2301,17 +2287,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-preprocess@5.1.3(svelte@3.57.0)(typescript@5.4.5): - dependencies: - '@types/pug': 2.0.6 - detect-indent: 6.1.0 - magic-string: 0.30.7 - sorcery: 0.11.0 - strip-indent: 3.0.0 - svelte: 3.57.0 - optionalDependencies: - typescript: 5.4.5 - svelte-preprocess@5.1.3(svelte@3.57.0)(typescript@5.5.2): dependencies: '@types/pug': 2.0.6 @@ -2360,8 +2335,6 @@ snapshots: dependencies: semver: 7.5.1 - typescript@5.4.5: {} - typescript@5.5.2: {} unist-util-stringify-position@3.0.3: From 6fda024e93d0ced68283839de40a684b1ea2721b Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 31 Jul 2024 17:20:31 +0200 Subject: [PATCH 05/36] breaking: require node 18 or later --- packages/svelte-check/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/svelte-check/package.json b/packages/svelte-check/package.json index 3719a6efb..6357a4c7b 100644 --- a/packages/svelte-check/package.json +++ b/packages/svelte-check/package.json @@ -22,6 +22,9 @@ "url": "https://github.com/sveltejs/language-tools/issues" }, "homepage": "https://github.com/sveltejs/language-tools#readme", + "engines": { + "node": ">= 18.0.0" + }, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", "chokidar": "^3.4.1", From 03482d0c33d6d3b265d8ac0cad1e14e78d4270d5 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 31 Jul 2024 17:23:04 +0200 Subject: [PATCH 06/36] breaking: require Svelte 4 or later devDependencies pinned to Svelte 3 because other packages in this repo still use it. Theoretically we still support Svelte 3 with svelte-check v4 but this gives us the opportunity to adjust that later without a major --- packages/svelte-check/package.json | 3 ++- pnpm-lock.yaml | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/svelte-check/package.json b/packages/svelte-check/package.json index 6357a4c7b..9781ce03b 100644 --- a/packages/svelte-check/package.json +++ b/packages/svelte-check/package.json @@ -33,7 +33,7 @@ "svelte-preprocess": "^5.1.3" }, "peerDependencies": { - "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "scripts": { @@ -53,6 +53,7 @@ "rollup": "3.7.5", "rollup-plugin-cleanup": "^3.2.0", "rollup-plugin-copy": "^3.4.0", + "svelte": "^3.57.0", "svelte-language-server": "workspace:*", "typescript": "^5.5.2", "vscode-languageserver": "8.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca1fb581b..3e3a9706e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,9 +127,6 @@ importers: sade: specifier: ^1.7.4 version: 1.8.1 - svelte: - specifier: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 - version: 3.57.0 svelte-preprocess: specifier: ^5.1.3 version: 5.1.3(svelte@3.57.0)(typescript@5.5.2) @@ -167,6 +164,9 @@ importers: rollup-plugin-copy: specifier: ^3.4.0 version: 3.4.0 + svelte: + specifier: ^3.57.0 + version: 3.57.0 svelte-language-server: specifier: workspace:* version: link:../language-server From 856ac7f72b592cd1f6decceaa3f049f29b3a2511 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Tue, 6 Aug 2024 16:45:12 +0800 Subject: [PATCH 07/36] fix service test --- .../services/project-reference/paths/imported.ts | 1 + .../services/project-reference/paths/importing.svelte | 6 ++++++ .../project-reference/paths/tsconfig_sub.json | 10 ++++++++++ .../services/project-reference/tsconfig.json | 11 +++++++++++ 4 files changed, 28 insertions(+) create mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts create mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte create mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json create mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts new file mode 100644 index 000000000..2a9042acc --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts @@ -0,0 +1 @@ +export function hi(cb: (num: number) => string) {}; \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte new file mode 100644 index 000000000..674a85cc3 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json new file mode 100644 index 000000000..e496c36b6 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "composite": true, + "strict": true, + "paths": { + "hi": ["./imported.ts"] + } + } +} \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json new file mode 100644 index 000000000..83937370e --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": [], + "references": [ + { + "path": "./paths/tsconfig_sub.json" + } + ], + "compilerOptions": { + "types": [] + } +} From 116285fc47c07ca43cb1ad3832c4542cb9481902 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 7 Aug 2024 13:43:53 +0800 Subject: [PATCH 08/36] fix diagnostics test and flaky test the fix for diagnostic made the flaky test appears a lot more often --- .../src/lib/documents/DocumentManager.ts | 2 -- .../src/plugins/typescript/service.ts | 22 ++++++++++++++----- .../project-reference/paths/expectedv2.json | 1 + .../paths/imported.ts | 0 .../paths/input.svelte | 0 .../paths/tsconfig_sub.json | 0 .../tsconfig.json | 0 7 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json rename packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/{project-reference.only => project-reference}/paths/imported.ts (100%) rename packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/{project-reference.only => project-reference}/paths/input.svelte (100%) rename packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/{project-reference.only => project-reference}/paths/tsconfig_sub.json (100%) rename packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/{project-reference.only => project-reference}/tsconfig.json (100%) diff --git a/packages/language-server/src/lib/documents/DocumentManager.ts b/packages/language-server/src/lib/documents/DocumentManager.ts index 5d17ae4dd..1c2df541b 100644 --- a/packages/language-server/src/lib/documents/DocumentManager.ts +++ b/packages/language-server/src/lib/documents/DocumentManager.ts @@ -50,12 +50,10 @@ export class DocumentManager { // open state should only be updated when the document is closed document.openedByClient ||= openedByClient; document.setText(textDocument.text); - document.openedByClient = openedByClient; } else { document = this.createDocument(textDocument); document.openedByClient = openedByClient; this.documents.set(textDocument.uri, document); - document.openedByClient = openedByClient; this.notify('documentOpen', document); } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 98fe3cd08..130989862 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -432,9 +432,12 @@ async function createLanguageService( const filePath = document.getFilePath() || ''; const prevSnapshot = snapshotManager.get(filePath); - console.log(filePath, prevSnapshot?.isOpenedInClient(), document.openedByClient) - - if (prevSnapshot?.version === document.version) { + if ( + prevSnapshot?.version === document.version && + // In the test, there might be a new document instance with a different openedByClient + // In that case, Create a new snapshot otherwise the getClientFileNames won't include the new client file + prevSnapshot.isOpenedInClient() === document.openedByClient + ) { return prevSnapshot; } @@ -815,6 +818,9 @@ async function createLanguageService( } exportMapCache.releaseSymbols(); + // https://github.com/microsoft/TypeScript/blob/941d1543c201e40d87e63c9db04818493afdd9e7/src/server/project.ts#L1731 + // if one file change results in clearing the cache + // don't continue to check other files, this will mark the cache as usable while it's empty for (const fileName of changedFilesForExportCache) { const oldFile = oldProgram.getSourceFile(fileName); const newFile = program?.getSourceFile(fileName); @@ -824,11 +830,15 @@ async function createLanguageService( continue; } - if (oldFile && newFile) { - exportMapCache.onFileChanged?.(oldFile, newFile, false); - } else { + if (!oldFile || !newFile) { // new file or deleted file exportMapCache.clear(); + break; + } + + const cleared = exportMapCache.onFileChanged?.(oldFile, newFile, false); + if (cleared) { + break; } } changedFilesForExportCache.clear(); diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/imported.ts b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/imported.ts similarity index 100% rename from packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/imported.ts rename to packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/imported.ts diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/input.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/input.svelte similarity index 100% rename from packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/input.svelte rename to packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/input.svelte diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/tsconfig_sub.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/tsconfig_sub.json similarity index 100% rename from packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/paths/tsconfig_sub.json rename to packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/tsconfig_sub.json diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/tsconfig.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/tsconfig.json similarity index 100% rename from packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference.only/tsconfig.json rename to packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/tsconfig.json From e98aec2f777dff466712f483ff0de4290565422c Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 7 Aug 2024 14:43:23 +0800 Subject: [PATCH 09/36] now I remember why the test file doesn't exist --- .../src/plugins/typescript/module-loader.ts | 12 ++-- .../project-reference/paths/imported.ts | 4 ++ .../test/plugins/typescript/service.test.ts | 56 ------------------- .../project-reference/paths/imported.ts | 1 - .../project-reference/paths/importing.svelte | 6 -- .../project-reference/paths/tsconfig_sub.json | 10 ---- .../services/project-reference/tsconfig.json | 11 ---- 7 files changed, 11 insertions(+), 89 deletions(-) delete mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts delete mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte delete mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json delete mode 100644 packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index c8ff7c374..e58b3b3cc 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -107,7 +107,7 @@ class ImpliedNodeFormatResolver { let mode = undefined; if (sourceFile) { this.cacheImpliedNodeFormat(sourceFile, compilerOptions); - mode = ts.getModeForResolutionAtIndex(sourceFile, importIdxInFile); + mode = ts.getModeForResolutionAtIndex(sourceFile, importIdxInFile, compilerOptions); } return mode; } @@ -230,7 +230,7 @@ export function createSvelteModuleLoader( containingFile: string, _reusedNames: string[] | undefined, redirectedReference: ts.ResolvedProjectReference | undefined, - _options: ts.CompilerOptions, + options: ts.CompilerOptions, containingSourceFile?: ts.SourceFile | undefined ): Array { return moduleNames.map((moduleName, index) => { @@ -243,7 +243,8 @@ export function createSvelteModuleLoader( containingFile, containingSourceFile, index, - redirectedReference + redirectedReference, + options ); resolvedModule?.failedLookupLocations?.forEach((failedLocation) => { @@ -262,13 +263,14 @@ export function createSvelteModuleLoader( containingFile: string, containingSourceFile: ts.SourceFile | undefined, index: number, - redirectedReference: ts.ResolvedProjectReference | undefined + redirectedReference: ts.ResolvedProjectReference | undefined, + option: ts.CompilerOptions ): ts.ResolvedModuleWithFailedLookupLocations { const mode = impliedNodeFormatResolver.resolve( name, index, containingSourceFile, - compilerOptions + option ); const resolvedModuleWithFailedLookup = tsModule.resolveModuleName( name, diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/imported.ts b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/imported.ts index 2a9042acc..b299729ae 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/imported.ts +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/imported.ts @@ -1 +1,5 @@ +/** + * + * @param cb callback because if the module resolution failed there will be a noImplicitAny error + */ export function hi(cb: (num: number) => string) {}; \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index 03899f995..503c783b2 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -269,62 +269,6 @@ describe('service', () => { }); }); - it('resolve module with the compilerOptions from project reference', async () => { - // Don't test this with module not found diagnostics - // because there is a case where is won't have an error but still not resolved the module - - const { lsAndTsDocResolver, docManager } = setupRealFS(); - const pathsProjectRefDir = path.join(serviceTestDir, 'project-reference', 'paths'); - - const importingFilePath = path.join(pathsProjectRefDir, 'importing.svelte'); - const document = docManager.openClientDocument({ - uri: pathToUrl(importingFilePath), - text: ts.sys.readFile(importingFilePath) || '' - }); - - const lsContainer = await lsAndTsDocResolver.getTSService(importingFilePath); - lsContainer.getService(); - const snapshotManager = lsContainer.snapshotManager; - - const importedFilePath = path.join(pathsProjectRefDir, 'imported.ts'); - assert.equal( - snapshotManager.has(importedFilePath), - false, - 'expected to load imported file through module resolution' - ); - - // uncomment the import - document.update( - '', - document.offsetAt({ - line: 2, - character: 0 - }), - document.offsetAt({ - line: 2, - character: 2 - }) - ); - lsContainer.updateSnapshot(document); - lsContainer.getService(); - - assert.ok(snapshotManager.has(importedFilePath)); - }); - - function setupRealFS() { - const docManager = new DocumentManager( - (textDocument) => new Document(textDocument.uri, textDocument.text) - ); - const lsConfigManager = new LSConfigManager(); - const workspaceUris = [pathToUrl(serviceTestDir)]; - const lsAndTsDocResolver = new LSAndTSDocResolver( - docManager, - workspaceUris, - lsConfigManager - ); - - return { lsAndTsDocResolver, docManager }; - } it('skip directory watching if directory is root', async () => { const dirPath = getRandomVirtualDirPath(path.join(testDir, 'Test')); const { virtualSystem, lsDocumentContext } = setup(); diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts deleted file mode 100644 index 2a9042acc..000000000 --- a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/imported.ts +++ /dev/null @@ -1 +0,0 @@ -export function hi(cb: (num: number) => string) {}; \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte deleted file mode 100644 index 674a85cc3..000000000 --- a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/importing.svelte +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json deleted file mode 100644 index e496c36b6..000000000 --- a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/paths/tsconfig_sub.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "composite": true, - "strict": true, - "paths": { - "hi": ["./imported.ts"] - } - } -} \ No newline at end of file diff --git a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json b/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json deleted file mode 100644 index 83937370e..000000000 --- a/packages/language-server/test/plugins/typescript/testfiles/services/project-reference/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "include": [], - "references": [ - { - "path": "./paths/tsconfig_sub.json" - } - ], - "compilerOptions": { - "types": [] - } -} From 01d111eae8a9f3dd5014dab9260c89d1bec60693 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 7 Aug 2024 15:59:17 +0800 Subject: [PATCH 10/36] test for source project reference redirect --- .../src/plugins/typescript/service.ts | 9 ++- .../src/plugins/typescript/serviceCache.ts | 2 +- .../test/plugins/typescript/service.test.ts | 75 +++++++++++++++++-- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 130989862..34b9abce7 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -258,6 +258,9 @@ async function createLanguageService( ); let svelteTsPath: string; + /** + * set and clear during program creation, shouldn't not be cached elsewhere + */ let compilerHost: ts.CompilerHost | undefined; try { // For when svelte2tsx/svelte-check is part of node_modules, for example VS Code extension @@ -323,9 +326,9 @@ async function createLanguageService( getModuleResolutionCache: svelteModuleLoader.getModuleResolutionCache, useSourceOfProjectReferenceRedirect() { return !languageServiceReducedMode; - } - // setCompilerHost: (host) => (compilerHost = host), - // getCompilerHost: () => compilerHost, + }, + setCompilerHost: (host) => (compilerHost = host), + getCompilerHost: () => compilerHost, }; const documentRegistry = getOrCreateDocumentRegistry( diff --git a/packages/language-server/src/plugins/typescript/serviceCache.ts b/packages/language-server/src/plugins/typescript/serviceCache.ts index b47d298d0..6cfc1ef0f 100644 --- a/packages/language-server/src/plugins/typescript/serviceCache.ts +++ b/packages/language-server/src/plugins/typescript/serviceCache.ts @@ -83,7 +83,7 @@ export function createProject( 'getPackageJsonAutoImportProvider', 'includePackageJsonAutoImports', // Volar doesn't have the "languageServiceReducedMode" support but we do - // so don't proxy this method and use our own implementation + // so don't proxy this method and implement this directly in the ts.LanguageServiceHost // 'useSourceOfProjectReferenceRedirect' ]; proxyMethods.forEach((key) => ((host as any)[key] = project[key].bind(project))); diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index 503c783b2..68f89164f 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -1,18 +1,16 @@ import assert from 'assert'; import path from 'path'; +import sinon from 'sinon'; import ts from 'typescript'; -import { Document, DocumentManager } from '../../../src/lib/documents'; +import { RelativePattern } from 'vscode-languageserver-protocol'; +import { Document } from '../../../src/lib/documents'; import { GlobalSnapshotsManager } from '../../../src/plugins/typescript/SnapshotManager'; import { LanguageServiceDocumentContext, getService } from '../../../src/plugins/typescript/service'; -import { pathToUrl } from '../../../src/utils'; +import { normalizePath, pathToUrl } from '../../../src/utils'; import { createVirtualTsSystem, getRandomVirtualDirPath } from './test-utils'; -import { LSConfigManager } from '../../../src/ls-config'; -import { LSAndTSDocResolver } from '../../../src/plugins'; -import sinon from 'sinon'; -import { RelativePattern } from 'vscode-languageserver-protocol'; describe('service', () => { const testDir = path.join(__dirname, 'testfiles'); @@ -269,6 +267,71 @@ describe('service', () => { }); }); + it('resolve module with source project reference redirect', async () => { + const dirPath = getRandomVirtualDirPath(testDir); + const { virtualSystem, lsDocumentContext, rootUris } = setup(); + + const package1 = path.join(dirPath, 'package1'); + + virtualSystem.writeFile( + path.join(package1, 'tsconfig.json'), + JSON.stringify({ + references: [{ path: '../package2' }], + files: ['index.ts'] + }) + ); + + const package2 = path.join(dirPath, 'package2'); + virtualSystem.writeFile( + path.join(package2, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + composite: true, + strict: true + }, + files: ['index.ts'] + }) + ); + + const importing = path.join(package1, 'index.ts'); + virtualSystem.writeFile( + importing, + 'import { hi } from "package2"; hi((a) => `${a}`);' + ); + + const imported = path.join(package2, 'index.ts'); + virtualSystem.writeFile(imported, 'export function hi(cb: (num: number) => string) {}'); + + const package2Link = normalizePath(path.join(package1, 'node_modules', 'package2')); + virtualSystem.realpath = (p) => { + if (normalizePath(p).startsWith(package2Link)) { + const sub = p.substring(package2Link.length); + return path.join(package2) + sub; + } + + return p; + }; + + const fileExists = virtualSystem.fileExists; + virtualSystem.fileExists = (p) => { + const realPath = virtualSystem.realpath!(p); + + return fileExists(realPath); + } + + const ls = await getService( + path.join(package1, 'DoNotMatter.svelte'), + rootUris, + lsDocumentContext + ); + + const service = ls.getService(); + assert.deepStrictEqual( + [], + service.getSemanticDiagnostics(importing).map((d) => d.messageText) + ); + }); + it('skip directory watching if directory is root', async () => { const dirPath = getRandomVirtualDirPath(path.join(testDir, 'Test')); const { virtualSystem, lsDocumentContext } = setup(); From 3131eb1e792a8a32f3ada1bc329c44f6c63fd3fe Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Mon, 12 Aug 2024 11:44:03 +0800 Subject: [PATCH 11/36] cache ParsedCommandLine, open ls for referenced tsconfig --- .../src/plugins/typescript/SnapshotManager.ts | 5 + .../src/plugins/typescript/module-loader.ts | 7 +- .../src/plugins/typescript/service.ts | 291 +++++++++++++----- .../src/plugins/typescript/serviceCache.ts | 2 +- .../project-reference/paths/expectedv2.json | 2 +- .../project-reference/paths/imported.ts | 4 +- .../project-reference/paths/tsconfig_sub.json | 2 +- .../plugins/typescript/module-loader.test.ts | 3 +- .../test/plugins/typescript/service.test.ts | 106 +++++-- .../test/plugins/typescript/test-utils.ts | 5 + 10 files changed, 314 insertions(+), 113 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/SnapshotManager.ts b/packages/language-server/src/plugins/typescript/SnapshotManager.ts index 6bf64f0b6..85b152e49 100644 --- a/packages/language-server/src/plugins/typescript/SnapshotManager.ts +++ b/packages/language-server/src/plugins/typescript/SnapshotManager.ts @@ -262,6 +262,11 @@ export class SnapshotManager { return Array.from(this.projectFileToOriginalCasing.values()); } + isProjectFile(fileName: string): boolean { + fileName = normalizePath(fileName); + return this.projectFileToOriginalCasing.has(this.getCanonicalFileName(fileName)); + } + private logStatistics() { const date = new Date(); // Don't use setInterval because that will keep tests running forever diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index e58b3b3cc..915e50b26 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -266,12 +266,7 @@ export function createSvelteModuleLoader( redirectedReference: ts.ResolvedProjectReference | undefined, option: ts.CompilerOptions ): ts.ResolvedModuleWithFailedLookupLocations { - const mode = impliedNodeFormatResolver.resolve( - name, - index, - containingSourceFile, - option - ); + const mode = impliedNodeFormatResolver.resolve(name, index, containingSourceFile, option); const resolvedModuleWithFailedLookup = tsModule.resolveModuleName( name, containingFile, diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 34b9abce7..49f790fb9 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -6,7 +6,13 @@ import { Document } from '../../lib/documents'; import { configLoader } from '../../lib/documents/configLoader'; import { FileMap, FileSet } from '../../lib/documents/fileCollection'; import { Logger } from '../../logger'; -import { createGetCanonicalFileName, normalizePath, pathToUrl, urlToPath } from '../../utils'; +import { + createGetCanonicalFileName, + isNotNullOrUndefined, + normalizePath, + pathToUrl, + urlToPath +} from '../../utils'; import { DocumentSnapshot, SvelteSnapshotOptions } from './DocumentSnapshot'; import { createSvelteModuleLoader } from './module-loader'; import { @@ -27,9 +33,6 @@ export interface LanguageServiceContainer { readonly tsconfigPath: string; readonly compilerOptions: ts.CompilerOptions; readonly configErrors: ts.Diagnostic[]; - /** - * @internal Public for tests only - */ readonly snapshotManager: SnapshotManager; getService(skipSynchronize?: boolean): ts.LanguageService; updateSnapshot(documentOrFilePath: Document | string): DocumentSnapshot; @@ -51,6 +54,8 @@ export interface LanguageServiceContainer { onPackageJsonChange(packageJsonPath: string): void; getTsConfigSvelteOptions(): { namespace: string }; + getProjectReferences(): ProjectReferenceInfo[]; + dispose(): void; } @@ -87,12 +92,19 @@ declare module 'typescript' { } } +export interface ProjectReferenceInfo { + parsedCommandLine: ts.ParsedCommandLine; + snapshotManager: SnapshotManager; + pendingProjectFileUpdate: boolean; + configFilePath: string; +} + const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; // 20 MB const services = new FileMap>(); const serviceSizeMap = new FileMap(); const configWatchers = new FileMap(); -const extendedConfigWatchers = new FileMap(); -const extendedConfigToTsConfigPath = new FileMap(); +const dependedConfigWatchers = new FileMap(); +const configPathToDependedProject = new FileMap(); const configFileModifiedTime = new FileMap(); const configFileForOpenFiles = new FileMap(); const pendingReloads = new FileSet(); @@ -117,7 +129,7 @@ export interface LanguageServiceDocumentContext { globalSnapshotsManager: GlobalSnapshotsManager; notifyExceedSizeLimit: (() => void) | undefined; extendedConfigCache: Map; - onProjectReloaded: (() => void) | undefined; + onProjectReloaded: ((configFileNames: string[]) => void) | undefined; watchTsConfig: boolean; tsSystem: ts.System; projectService: ProjectService | undefined; @@ -139,8 +151,16 @@ export async function getService( findTsConfigPath(path, workspaceUris, docContext.tsSystem.fileExists, getCanonicalFileName); if (tsconfigPath) { - configFileForOpenFiles.set(path, tsconfigPath); - return getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), docContext); + const needAssign = !configFileForOpenFiles.has(path); + let service = await getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), docContext); + if (!needAssign) { + configFileForOpenFiles.set(path, tsconfigPath); + return service; + } + + service = await findDefaultServiceForFile(path, service, docContext); + configFileForOpenFiles.set(path, service.tsconfigPath); + return service; } // Find closer boundary: workspace uri or node_modules @@ -163,6 +183,34 @@ export async function getService( ); } +function findDefaultServiceForFile( + filePath: string, + service: LanguageServiceContainer, + docContext: LanguageServiceDocumentContext +) { + const projectReferences = service.getProjectReferences(); + if (projectReferences.length === 0 || service.snapshotManager.isProjectFile(filePath)) { + return service; + } + + let possibleDefaultProject: ProjectReferenceInfo | undefined; + for (const element of projectReferences) { + if (element.snapshotManager.isProjectFile(filePath)) { + possibleDefaultProject = element; + break; + } + } + const defaultProject = projectReferences.find((p) => p.snapshotManager.isProjectFile(filePath)); + + return defaultProject + ? getServiceForTsconfig( + defaultProject.configFilePath, + dirname(defaultProject.configFilePath), + docContext + ) + : service; +} + export async function forAllServices( cb: (service: LanguageServiceContainer) => any ): Promise { @@ -221,29 +269,13 @@ async function createLanguageService( ): Promise { const { tsSystem } = docContext; - const { - options: compilerOptions, - errors: configErrors, - fileNames: files, - projectReferences, - raw, - extendedConfigPaths, - wildcardDirectories - } = getParsedConfig(); + const projectConfig = getParsedConfig(); + const { options: compilerOptions, raw, errors: configErrors } = projectConfig; const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); - watchWildCardDirectories(); - - // raw is the tsconfig merged with extending config - // see: https://github.com/microsoft/TypeScript/blob/08e4f369fbb2a5f0c30dee973618d65e6f7f09f8/src/compiler/commandLineParser.ts#L2537 - const snapshotManager = new SnapshotManager( - docContext.globalSnapshotsManager, - raw, - workspacePath, - tsSystem, - files, - wildcardDirectories - ); + watchWildCardDirectories(projectConfig); + + const snapshotManager = createSnapshotManager(projectConfig); // 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. @@ -288,6 +320,7 @@ async function createLanguageService( : './svelte-jsx-v4.d.ts'; const changedFilesForExportCache = new Set(); + const projectReferenceInfo = new Map(); const svelteTsxFiles = ( isSvelte3 @@ -315,7 +348,8 @@ async function createLanguageService( readDirectory: svelteModuleLoader.readDirectory, realpath: tsSystem.realpath, getDirectories: tsSystem.getDirectories, - getProjectReferences: () => projectReferences, + getProjectReferences: () => projectConfig.projectReferences, + getParsedCommandLine, useCaseSensitiveFileNames: () => tsSystem.useCaseSensitiveFileNames, getScriptKind: (fileName: string) => getSnapshot(fileName).scriptKind, getProjectVersion: () => projectVersion.toString(), @@ -328,11 +362,12 @@ async function createLanguageService( return !languageServiceReducedMode; }, setCompilerHost: (host) => (compilerHost = host), - getCompilerHost: () => compilerHost, + getCompilerHost: () => compilerHost }; const documentRegistry = getOrCreateDocumentRegistry( - host.getCurrentDirectory(), + // this should mostly be a singleton while host.getCurrentDirectory() might be the directory where the tsconfig is + tsSystem.getCurrentDirectory(), tsSystem.useCaseSensitiveFileNames ); @@ -349,8 +384,7 @@ async function createLanguageService( docContext.globalSnapshotsManager.onChange(scheduleUpdate); reduceLanguageServiceCapabilityIfFileSizeTooBig(); - updateExtendedConfigDependents(); - watchConfigFile(); + watchConfigFiles(projectConfig.extendedConfigPaths, projectConfig); return { tsconfigPath, @@ -368,10 +402,25 @@ async function createLanguageService( onAutoImportProviderSettingsChanged, onPackageJsonChange, getTsConfigSvelteOptions, + getProjectReferences, dispose }; - function watchWildCardDirectories() { + function createSnapshotManager(parsedCommandLine: ts.ParsedCommandLine) { + // raw is the tsconfig merged with extending config + // see: https://github.com/microsoft/TypeScript/blob/08e4f369fbb2a5f0c30dee973618d65e6f7f09f8/src/compiler/commandLineParser.ts#L2537 + return new SnapshotManager( + docContext.globalSnapshotsManager, + parsedCommandLine.raw, + workspacePath, + tsSystem, + parsedCommandLine.fileNames, + parsedCommandLine.wildcardDirectories + ); + } + + function watchWildCardDirectories(parseCommandLine: ts.ParsedCommandLine) { + const { wildcardDirectories } = parseCommandLine; if (!wildcardDirectories || !docContext.watchDirectory) { return; } @@ -510,12 +559,17 @@ async function createLanguageService( } function scheduleProjectFileUpdate(watcherNewFiles: string[]): void { - if (snapshotManager.areIgnoredFromNewFileWatch(watcherNewFiles)) { - return; + if (!snapshotManager.areIgnoredFromNewFileWatch(watcherNewFiles)) { + scheduleUpdate(); + pendingProjectFileUpdate = true; } - scheduleUpdate(); - pendingProjectFileUpdate = true; + for (const config of projectReferenceInfo.values()) { + if (config && !config.snapshotManager.areIgnoredFromNewFileWatch(watcherNewFiles)) { + config.pendingProjectFileUpdate = true; + scheduleUpdate(); + } + } } function updateProjectFiles(): void { @@ -588,23 +642,7 @@ async function createLanguageService( ); } - const extendedConfigPaths = new Set(); - const { extendedConfigCache } = docContext; - const cacheMonitorProxy = { - ...docContext.extendedConfigCache, - get(key: string) { - extendedConfigPaths.add(key); - return extendedConfigCache.get(key); - }, - has(key: string) { - extendedConfigPaths.add(key); - return extendedConfigCache.has(key); - }, - set(key: string, value: ts.ExtendedConfigCacheEntry) { - extendedConfigPaths.add(key); - return extendedConfigCache.set(key, value); - } - }; + const { cacheMonitorProxy, extendedConfigPaths } = monitorExtendedConfig(); const parsedConfig = ts.parseJsonConfigFileContent( configJson, @@ -613,16 +651,7 @@ async function createLanguageService( forcedCompilerOptions, tsconfigPath, undefined, - [ - { - extension: 'svelte', - isMixedContent: true, - // Deferred was added in a later TS version, fall back to tsx - // If Deferred exists, this means that all Svelte files are included - // in parsedConfig.fileNames - scriptKind: ts.ScriptKind.Deferred ?? ts.ScriptKind.TS - } - ], + getExtraExtensions(), cacheMonitorProxy ); @@ -677,6 +706,40 @@ async function createLanguageService( }; } + function monitorExtendedConfig() { + const extendedConfigPaths = new Set(); + const { extendedConfigCache } = docContext; + const cacheMonitorProxy = { + ...docContext.extendedConfigCache, + get(key: string) { + extendedConfigPaths.add(key); + return extendedConfigCache.get(key); + }, + has(key: string) { + extendedConfigPaths.add(key); + return extendedConfigCache.has(key); + }, + set(key: string, value: ts.ExtendedConfigCacheEntry) { + extendedConfigPaths.add(key); + return extendedConfigCache.set(key, value); + } + }; + return { cacheMonitorProxy, extendedConfigPaths }; + } + + function getExtraExtensions(): readonly ts.FileExtensionInfo[] | undefined { + return [ + { + extension: 'svelte', + isMixedContent: true, + // Deferred was added in a later TS version, fall back to tsx + // If Deferred exists, this means that all Svelte files are included + // in parsedConfig.fileNames + scriptKind: ts.ScriptKind.Deferred ?? ts.ScriptKind.TS + } + ]; + } + /** * This should only be used when there's no jsconfig/tsconfig at all */ @@ -731,19 +794,23 @@ async function createLanguageService( docContext.globalSnapshotsManager.removeChangeListener(scheduleUpdate); } - function updateExtendedConfigDependents() { - extendedConfigPaths.forEach((extendedConfig) => { - let dependedTsConfig = extendedConfigToTsConfigPath.get(extendedConfig); + function watchConfigFiles( + extendedConfigPaths: Set, + parsedCommandLine: ts.ParsedCommandLine + ) { + const tsconfigDependencies = Array.from(extendedConfigPaths).concat( + parsedCommandLine.projectReferences?.map((r) => r.path) ?? [] + ); + tsconfigDependencies.forEach((configPath) => { + let dependedTsConfig = configPathToDependedProject.get(configPath); if (!dependedTsConfig) { dependedTsConfig = new FileSet(tsSystem.useCaseSensitiveFileNames); - extendedConfigToTsConfigPath.set(extendedConfig, dependedTsConfig); + configPathToDependedProject.set(configPath, dependedTsConfig); } dependedTsConfig.add(tsconfigPath); }); - } - function watchConfigFile() { if (!tsSystem.watchFile || !docContext.watchTsConfig) { return; } @@ -757,16 +824,16 @@ async function createLanguageService( ); } - for (const config of extendedConfigPaths) { - if (extendedConfigWatchers.has(config)) { + for (const config of tsconfigDependencies) { + if (dependedConfigWatchers.has(config)) { continue; } configFileModifiedTime.set(config, tsSystem.getModifiedTime?.(config)); - extendedConfigWatchers.set( + dependedConfigWatchers.set( config, // for some reason setting the polling interval is necessary, else some error in TS is thrown - tsSystem.watchFile(config, createWatchExtendedConfigCallback(docContext), 1000) + tsSystem.watchFile(config, createWatchDependedConfigCallback(docContext), 1000) ); } } @@ -792,7 +859,7 @@ async function createLanguageService( configFileForOpenFiles.clear(); } - docContext.onProjectReloaded?.(); + docContext.onProjectReloaded?.([fileName]); } function updateIfDirty() { @@ -912,6 +979,66 @@ async function createLanguageService( namespace: transformationConfig.typingsNamespace }; } + + function getParsedCommandLine(configFilePath: string): ts.ParsedCommandLine | undefined { + const cached = projectReferenceInfo.get(tsconfigPath); + if (cached !== undefined) { + if (cached?.pendingProjectFileUpdate) { + cached.pendingProjectFileUpdate = false; + cached.snapshotManager.updateProjectFiles(); + cached.parsedCommandLine.fileNames = cached.snapshotManager.getProjectFileNames(); + } + + return cached?.parsedCommandLine; + } + + const content = tsSystem.fileExists(configFilePath) && tsSystem.readFile(configFilePath); + if (!content) { + projectReferenceInfo.set(tsconfigPath, null); + return undefined; + } + + const json = ts.parseJsonText(configFilePath, content); + + const { cacheMonitorProxy, extendedConfigPaths } = monitorExtendedConfig(); + // TypeScript will throw if the parsedCommandLine doesn't include the sourceFile for the config file + // i.e. it must be directly parse from the json text instead of a javascript object like we do in getParsedConfig + const parsedCommandLine = ts.parseJsonSourceFileConfigFileContent( + json, + tsSystem, + dirname(configFilePath), + /*existingOptions*/ undefined, + configFilePath, + /*resolutionStack*/ undefined, + getExtraExtensions(), + cacheMonitorProxy + ); + + if (parsedCommandLine.errors.length) { + configErrors.push(...parsedCommandLine.errors); + } + + const snapshotManager = createSnapshotManager(parsedCommandLine); + + projectReferenceInfo.set(configFilePath, { + parsedCommandLine, + snapshotManager, + pendingProjectFileUpdate: false, + configFilePath + }); + + watchConfigFiles(extendedConfigPaths, parsedCommandLine); + + return parsedCommandLine; + } + + function getProjectReferences(): ProjectReferenceInfo[] { + if (projectConfig.projectReferences?.length) { + languageService.getProgram(); // ensure program is up to date + } + + return Array.from(projectReferenceInfo.values()).filter(isNotNullOrUndefined); + } } /** @@ -971,7 +1098,7 @@ function exceedsTotalSizeLimitForNonTsFiles( * because it would reference the closure * So that GC won't drop it and cause memory leaks */ -function createWatchExtendedConfigCallback(docContext: LanguageServiceDocumentContext) { +function createWatchDependedConfigCallback(docContext: LanguageServiceDocumentContext) { return async ( fileName: string, kind: ts.FileWatcherEventKind, @@ -989,8 +1116,10 @@ function createWatchExtendedConfigCallback(docContext: LanguageServiceDocumentCo docContext.extendedConfigCache.delete(fileName); - const promises = Array.from(extendedConfigToTsConfigPath.get(fileName) ?? []).map( + const reloadingConfigs: string[] = []; + const promises = Array.from(configPathToDependedProject.get(fileName) ?? []).map( async (config) => { + reloadingConfigs.push(config); const oldService = services.get(config); scheduleReload(config); (await oldService)?.dispose(); @@ -998,7 +1127,7 @@ function createWatchExtendedConfigCallback(docContext: LanguageServiceDocumentCo ); await Promise.all(promises); - docContext.onProjectReloaded?.(); + docContext.onProjectReloaded?.(reloadingConfigs); }; } diff --git a/packages/language-server/src/plugins/typescript/serviceCache.ts b/packages/language-server/src/plugins/typescript/serviceCache.ts index 6cfc1ef0f..d91b05b32 100644 --- a/packages/language-server/src/plugins/typescript/serviceCache.ts +++ b/packages/language-server/src/plugins/typescript/serviceCache.ts @@ -81,7 +81,7 @@ export function createProject( 'getSymlinkCache', 'getPackageJsonsVisibleToFile', 'getPackageJsonAutoImportProvider', - 'includePackageJsonAutoImports', + 'includePackageJsonAutoImports' // Volar doesn't have the "languageServiceReducedMode" support but we do // so don't proxy this method and implement this directly in the ts.LanguageServiceHost // 'useSourceOfProjectReferenceRedirect' diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json index 0637a088a..fe51488c7 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json @@ -1 +1 @@ -[] \ No newline at end of file +[] diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/imported.ts b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/imported.ts index b299729ae..bc8481bbb 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/imported.ts +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/imported.ts @@ -1,5 +1,5 @@ /** - * + * * @param cb callback because if the module resolution failed there will be a noImplicitAny error */ -export function hi(cb: (num: number) => string) {}; \ No newline at end of file +export function hi(cb: (num: number) => string) {} diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/tsconfig_sub.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/tsconfig_sub.json index e496c36b6..521061053 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/tsconfig_sub.json +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/tsconfig_sub.json @@ -7,4 +7,4 @@ "hi": ["./imported.ts"] } } -} \ No newline at end of file +} 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 6f84335c8..559eeb447 100644 --- a/packages/language-server/test/plugins/typescript/module-loader.test.ts +++ b/packages/language-server/test/plugins/typescript/module-loader.test.ts @@ -23,9 +23,8 @@ describe('createSvelteModuleLoader', () => { }; const moduleResolutionHost = { ...ts.sys }; - const svelteSys = { - ...svS.createSvelteSys(ts.sys), + ...svS.createSvelteSys(ts.sys) }; sinon.stub(svS, 'createSvelteSys').returns(svelteSys); diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index 68f89164f..8f5fb5eae 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -6,6 +6,7 @@ import { RelativePattern } from 'vscode-languageserver-protocol'; import { Document } from '../../../src/lib/documents'; import { GlobalSnapshotsManager } from '../../../src/plugins/typescript/SnapshotManager'; import { + LanguageServiceContainer, LanguageServiceDocumentContext, getService } from '../../../src/plugins/typescript/service'; @@ -14,7 +15,6 @@ import { createVirtualTsSystem, getRandomVirtualDirPath } from './test-utils'; describe('service', () => { const testDir = path.join(__dirname, 'testfiles'); - const serviceTestDir = path.join(testDir, 'services'); function setup() { const virtualSystem = createVirtualTsSystem(testDir); @@ -126,21 +126,24 @@ describe('service', () => { function createReloadTester( docContext: LanguageServiceDocumentContext, - testAfterReload: () => Promise + testAfterReload: (reloadingConfigs: string[]) => Promise ) { let _resolve: () => void; - const reloadPromise = new Promise((resolve) => { + let _reject: (e: unknown) => void; + const reloadPromise = new Promise((resolve, reject) => { _resolve = resolve; + _reject = reject; }); return { docContextWithReload: { ...docContext, - async onProjectReloaded() { + async onProjectReloaded(reloadingConfigs: string[]) { try { - await testAfterReload(); - } finally { + await testAfterReload(reloadingConfigs); _resolve(); + } catch (e) { + _reject(e); } } }, @@ -190,6 +193,8 @@ describe('service', () => { true, 'expected to reload compilerOptions' ); + + return true; } }); @@ -198,7 +203,7 @@ describe('service', () => { const { virtualSystem, lsDocumentContext, rootUris } = setup(); const tsconfigPath = path.join(dirPath, 'tsconfig.json'); const extend = './.svelte-kit/tsconfig.json'; - const extendedConfigPathFull = path.resolve(tsconfigPath, extend); + const extendedConfigPathFull = path.resolve(path.dirname(tsconfigPath), extend); virtualSystem.writeFile( tsconfigPath, @@ -235,6 +240,69 @@ describe('service', () => { true, 'expected to reload compilerOptions' ); + return true; + } + }); + + it('can watch project reference tsconfig', async () => { + const dirPath = getRandomVirtualDirPath(testDir); + const { virtualSystem, lsDocumentContext, rootUris } = setup(); + const tsconfigPath = path.join(dirPath, 'tsconfig.json'); + const referenced = './tsconfig_node.json'; + const referencedConfigPathFull = path.resolve(path.dirname(tsconfigPath), referenced); + + virtualSystem.writeFile( + tsconfigPath, + JSON.stringify({ + references: [{ path: referenced }] + }) + ); + + virtualSystem.writeFile( + referencedConfigPathFull, + JSON.stringify({ + compilerOptions: { + strict: true + }, + files: ['random.ts'] + }) + ); + + const { reloadPromise, docContextWithReload } = createReloadTester( + { ...lsDocumentContext, watchTsConfig: true }, + testAfterReload + ); + + const tsFilePath = path.join(dirPath, 'random.ts'); + virtualSystem.writeFile(tsFilePath, 'const a: number = null;'); + + const ls = await getService(tsFilePath, rootUris, docContextWithReload); + assert.deepStrictEqual(getSemanticDiagnosticsMessages(ls, tsFilePath), [ + "Type 'null' is not assignable to type 'number'." + ]); + + virtualSystem.writeFile( + referencedConfigPathFull, + JSON.stringify({ + compilerOptions: { + strict: false + } + }) + ); + + await reloadPromise; + + async function testAfterReload(reloadingConfigs: string[]) { + if (!reloadingConfigs.includes(referencedConfigPathFull)) { + return false; + } + const newLs = await getService(tsFilePath, rootUris, { + ...lsDocumentContext, + watchTsConfig: true + }); + + assert.deepStrictEqual(getSemanticDiagnosticsMessages(newLs, tsFilePath), []); + return true; } }); @@ -294,13 +362,10 @@ describe('service', () => { ); const importing = path.join(package1, 'index.ts'); - virtualSystem.writeFile( - importing, - 'import { hi } from "package2"; hi((a) => `${a}`);' - ); + virtualSystem.writeFile(importing, 'import { hi } from "package2"; hi((a) => `${a}`);'); const imported = path.join(package2, 'index.ts'); - virtualSystem.writeFile(imported, 'export function hi(cb: (num: number) => string) {}'); + virtualSystem.writeFile(imported, 'export function hi(cb: (num: number) => string) {}'); const package2Link = normalizePath(path.join(package1, 'node_modules', 'package2')); virtualSystem.realpath = (p) => { @@ -315,9 +380,9 @@ describe('service', () => { const fileExists = virtualSystem.fileExists; virtualSystem.fileExists = (p) => { const realPath = virtualSystem.realpath!(p); - + return fileExists(realPath); - } + }; const ls = await getService( path.join(package1, 'DoNotMatter.svelte'), @@ -325,11 +390,7 @@ describe('service', () => { lsDocumentContext ); - const service = ls.getService(); - assert.deepStrictEqual( - [], - service.getSemanticDiagnostics(importing).map((d) => d.messageText) - ); + assert.deepStrictEqual(getSemanticDiagnosticsMessages(ls, importing), []); }); it('skip directory watching if directory is root', async () => { @@ -388,4 +449,11 @@ describe('service', () => { sinon.assert.calledWith(watchDirectory.firstCall, []); }); + + function getSemanticDiagnosticsMessages(ls: LanguageServiceContainer, filePath: string) { + return ls + .getService() + .getSemanticDiagnostics(filePath) + .map((d) => d.messageText); + } }); diff --git a/packages/language-server/test/plugins/typescript/test-utils.ts b/packages/language-server/test/plugins/typescript/test-utils.ts index c77970256..4810e99c1 100644 --- a/packages/language-server/test/plugins/typescript/test-utils.ts +++ b/packages/language-server/test/plugins/typescript/test-utils.ts @@ -110,6 +110,11 @@ export function createVirtualTsSystem(currentDirectory: string): ts.System { return virtualSystem; function triggerWatch(normalizedPath: string, kind: ts.FileWatcherEventKind) { + // if watcher is not set yet. don't trigger it + if (!watchers.has(normalizedPath)) { + return; + } + let timeoutsOfPath = watchTimeout.get(normalizedPath); if (!timeoutsOfPath) { From 2b12603250b74e1dcd7e5a26a8b5782aab62ace6 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Tue, 13 Aug 2024 15:17:25 +0800 Subject: [PATCH 12/36] reuse config and snapshot manager --- .../plugins/typescript/LSAndTSDocResolver.ts | 3 +- .../src/plugins/typescript/service.ts | 199 +++++++++++------- .../test/plugins/typescript/service.test.ts | 6 +- 3 files changed, 132 insertions(+), 76 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index 3843e8a89..aecf30cbf 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -121,7 +121,8 @@ export class LSAndTSDocResolver { watchDirectory: this.options?.watchDirectory ? this.watchDirectory.bind(this) : undefined, - nonRecursiveWatchPattern: this.options?.nonRecursiveWatchPattern + nonRecursiveWatchPattern: this.options?.nonRecursiveWatchPattern, + projectReferenceInfo: new Map() }; } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 49f790fb9..1dc69ada6 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -54,7 +54,7 @@ export interface LanguageServiceContainer { onPackageJsonChange(packageJsonPath: string): void; getTsConfigSvelteOptions(): { namespace: string }; - getProjectReferences(): ProjectReferenceInfo[]; + getProjectReferences(): TsConfigInfo[]; dispose(): void; } @@ -92,11 +92,12 @@ declare module 'typescript' { } } -export interface ProjectReferenceInfo { +export interface TsConfigInfo { parsedCommandLine: ts.ParsedCommandLine; snapshotManager: SnapshotManager; pendingProjectFileUpdate: boolean; configFilePath: string; + extendedConfigPaths?: Set; } const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; // 20 MB @@ -135,6 +136,7 @@ export interface LanguageServiceDocumentContext { projectService: ProjectService | undefined; watchDirectory: ((patterns: RelativePattern[]) => void) | undefined; nonRecursiveWatchPattern: string | undefined; + projectReferenceInfo: Map; } export async function getService( @@ -193,13 +195,9 @@ function findDefaultServiceForFile( return service; } - let possibleDefaultProject: ProjectReferenceInfo | undefined; - for (const element of projectReferences) { - if (element.snapshotManager.isProjectFile(filePath)) { - possibleDefaultProject = element; - break; - } - } + // might be possible that the file is a further nested project file + // but then we'll have to initialize a new service and the corresponding ts.Program to know + // so just skip this for now const defaultProject = projectReferences.find((p) => p.snapshotManager.isProjectFile(filePath)); return defaultProject @@ -275,7 +273,7 @@ async function createLanguageService( const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); watchWildCardDirectories(projectConfig); - const snapshotManager = createSnapshotManager(projectConfig); + const snapshotManager = createSnapshotManager(projectConfig, tsconfigPath); // 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. @@ -320,7 +318,6 @@ async function createLanguageService( : './svelte-jsx-v4.d.ts'; const changedFilesForExportCache = new Set(); - const projectReferenceInfo = new Map(); const svelteTsxFiles = ( isSvelte3 @@ -406,7 +403,16 @@ async function createLanguageService( dispose }; - function createSnapshotManager(parsedCommandLine: ts.ParsedCommandLine) { + function createSnapshotManager( + parsedCommandLine: ts.ParsedCommandLine, + configFileName: string + ) { + const cached = configFileName + ? docContext.projectReferenceInfo.get(configFileName) + : undefined; + if (cached?.snapshotManager) { + return cached.snapshotManager; + } // raw is the tsconfig merged with extending config // see: https://github.com/microsoft/TypeScript/blob/08e4f369fbb2a5f0c30dee973618d65e6f7f09f8/src/compiler/commandLineParser.ts#L2537 return new SnapshotManager( @@ -564,8 +570,14 @@ async function createLanguageService( pendingProjectFileUpdate = true; } - for (const config of projectReferenceInfo.values()) { - if (config && !config.snapshotManager.areIgnoredFromNewFileWatch(watcherNewFiles)) { + const projectReferences = getProjectReferences(); + for (const config of projectReferences) { + if ( + config && + // handled by the respective service + !services.has(config.configFilePath) && + !config.snapshotManager.areIgnoredFromNewFileWatch(watcherNewFiles) + ) { config.pendingProjectFileUpdate = true; scheduleUpdate(); } @@ -618,47 +630,22 @@ async function createLanguageService( } function getParsedConfig() { - const forcedCompilerOptions: ts.CompilerOptions = { - allowNonTsExtensions: true, - target: ts.ScriptTarget.Latest, - allowJs: true, - noEmit: true, - declaration: false, - skipLibCheck: true - }; - - // always let ts parse config to get default compilerOption - let configJson = - (tsconfigPath && ts.readConfigFile(tsconfigPath, tsSystem.readFile).config) || - getDefaultJsConfig(); - - // Only default exclude when no extends for now - if (!configJson.extends) { - configJson = Object.assign( - { - exclude: getDefaultExclude() - }, - configJson - ); + let compilerOptions: ts.CompilerOptions; + let parsedConfig: ts.ParsedCommandLine; + let extendedConfigPaths: Set; + + const exist = tsconfigPath && docContext.projectReferenceInfo.get(tsconfigPath); + if (exist) { + compilerOptions = exist.parsedCommandLine.options; + parsedConfig = exist.parsedCommandLine; + extendedConfigPaths = exist.extendedConfigPaths ?? new Set(); + } else { + const config = parseDefaultCompilerOptions(); + compilerOptions = config.compilerOptions; + parsedConfig = config.parsedConfig; + extendedConfigPaths = config.extendedConfigPaths; } - const { cacheMonitorProxy, extendedConfigPaths } = monitorExtendedConfig(); - - const parsedConfig = ts.parseJsonConfigFileContent( - configJson, - tsSystem, - workspacePath, - forcedCompilerOptions, - tsconfigPath, - undefined, - getExtraExtensions(), - cacheMonitorProxy - ); - - const compilerOptions: ts.CompilerOptions = { - ...parsedConfig.options, - ...forcedCompilerOptions - }; if ( !compilerOptions.moduleResolution || compilerOptions.moduleResolution === ts.ModuleResolutionKind.Classic @@ -706,6 +693,52 @@ async function createLanguageService( }; } + function parseDefaultCompilerOptions() { + const forcedCompilerOptions: ts.CompilerOptions = { + allowNonTsExtensions: true, + target: ts.ScriptTarget.Latest, + allowJs: true, + noEmit: true, + declaration: false, + skipLibCheck: true + }; + + // always let ts parse config to get default compilerOption + let configJson = + (tsconfigPath && ts.readConfigFile(tsconfigPath, tsSystem.readFile).config) || + getDefaultJsConfig(); + + // Only default exclude when no extends for now + if (!configJson.extends) { + configJson = Object.assign( + { + exclude: getDefaultExclude() + }, + configJson + ); + } + + const { cacheMonitorProxy, extendedConfigPaths } = monitorExtendedConfig(); + + const parsedConfig = ts.parseJsonConfigFileContent( + configJson, + tsSystem, + workspacePath, + forcedCompilerOptions, + tsconfigPath, + undefined, + getExtraExtensions(), + cacheMonitorProxy + ); + + const compilerOptions: ts.CompilerOptions = { + ...parsedConfig.options, + ...forcedCompilerOptions + }; + + return { compilerOptions, parsedConfig, extendedConfigPaths }; + } + function monitorExtendedConfig() { const extendedConfigPaths = new Set(); const { extendedConfigCache } = docContext; @@ -786,6 +819,7 @@ async function createLanguageService( } function dispose() { + compilerHost = undefined; languageService.dispose(); snapshotManager.dispose(); configWatchers.get(tsconfigPath)?.close(); @@ -980,22 +1014,18 @@ async function createLanguageService( }; } - function getParsedCommandLine(configFilePath: string): ts.ParsedCommandLine | undefined { - const cached = projectReferenceInfo.get(tsconfigPath); + function ensureTsConfigInfoUpToDate(configFilePath: string) { + const projectReferenceInfo = docContext.projectReferenceInfo; + const cached = projectReferenceInfo.get(configFilePath); if (cached !== undefined) { - if (cached?.pendingProjectFileUpdate) { - cached.pendingProjectFileUpdate = false; - cached.snapshotManager.updateProjectFiles(); - cached.parsedCommandLine.fileNames = cached.snapshotManager.getProjectFileNames(); - } - - return cached?.parsedCommandLine; + ensureProjectFileUpToDate(cached); + return cached; } const content = tsSystem.fileExists(configFilePath) && tsSystem.readFile(configFilePath); if (!content) { - projectReferenceInfo.set(tsconfigPath, null); - return undefined; + projectReferenceInfo.set(configFilePath, null); + return null; } const json = ts.parseJsonText(configFilePath, content); @@ -1014,30 +1044,53 @@ async function createLanguageService( cacheMonitorProxy ); + parsedCommandLine.options.allowNonTsExtensions = true; + if (parsedCommandLine.errors.length) { configErrors.push(...parsedCommandLine.errors); } - const snapshotManager = createSnapshotManager(parsedCommandLine); + const snapshotManager = createSnapshotManager(parsedCommandLine, configFilePath); - projectReferenceInfo.set(configFilePath, { + const tsconfigInfo: TsConfigInfo = { parsedCommandLine, snapshotManager, pendingProjectFileUpdate: false, - configFilePath - }); + configFilePath, + extendedConfigPaths + }; + projectReferenceInfo.set(configFilePath, tsconfigInfo); watchConfigFiles(extendedConfigPaths, parsedCommandLine); - return parsedCommandLine; + return tsconfigInfo; + } + + function getParsedCommandLine(configFilePath: string) { + const info = docContext.projectReferenceInfo.get(configFilePath); + if (info) { + return info.parsedCommandLine; + } + + return ensureTsConfigInfoUpToDate(configFilePath)?.parsedCommandLine; + } + + function ensureProjectFileUpToDate(info: TsConfigInfo | null) { + if (info?.pendingProjectFileUpdate) { + info.pendingProjectFileUpdate = false; + info.snapshotManager.updateProjectFiles(); + info.parsedCommandLine.fileNames = info.snapshotManager.getProjectFileNames(); + } } - function getProjectReferences(): ProjectReferenceInfo[] { - if (projectConfig.projectReferences?.length) { - languageService.getProgram(); // ensure program is up to date + function getProjectReferences(): TsConfigInfo[] { + if (!tsconfigPath || !projectConfig.projectReferences) { + return []; } - return Array.from(projectReferenceInfo.values()).filter(isNotNullOrUndefined); + return projectConfig.projectReferences + .map((ref) => ensureTsConfigInfoUpToDate(normalizePath(ref.path))) + .filter(isNotNullOrUndefined); } } diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index 8f5fb5eae..9511de96f 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -34,7 +34,8 @@ describe('service', () => { onProjectReloaded: undefined, projectService: undefined, nonRecursiveWatchPattern: undefined, - watchDirectory: undefined + watchDirectory: undefined, + projectReferenceInfo: new Map() }; return { virtualSystem, lsDocumentContext, rootUris }; @@ -254,7 +255,8 @@ describe('service', () => { virtualSystem.writeFile( tsconfigPath, JSON.stringify({ - references: [{ path: referenced }] + references: [{ path: referenced }], + include: [] }) ); From d2f4110d657bb8ac475e2063575a6c5efb7d6cc9 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Tue, 13 Aug 2024 16:27:03 +0800 Subject: [PATCH 13/36] reload TsConfigInfo --- packages/language-server/src/plugins/typescript/service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 1dc69ada6..0e6bf5183 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -246,6 +246,7 @@ export async function getServiceForTsconfig( } pendingReloads.delete(tsconfigPath); + docContext.projectReferenceInfo.delete(tsconfigPath); const newService = createLanguageService(tsconfigPath, workspacePath, docContext); services.set(tsconfigPathOrWorkspacePath, newService); service = await newService; From 3d25d4332fa9f31643ffe15064ed6a94d5969929 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Tue, 13 Aug 2024 17:44:49 +0800 Subject: [PATCH 14/36] make the scope consistent with service container in test, don't update while scheduling project file update --- .../plugins/typescript/LSAndTSDocResolver.ts | 3 +-- .../src/plugins/typescript/service.ts | 22 ++++++++----------- .../test/plugins/typescript/service.test.ts | 3 +-- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index aecf30cbf..3843e8a89 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -121,8 +121,7 @@ export class LSAndTSDocResolver { watchDirectory: this.options?.watchDirectory ? this.watchDirectory.bind(this) : undefined, - nonRecursiveWatchPattern: this.options?.nonRecursiveWatchPattern, - projectReferenceInfo: new Map() + nonRecursiveWatchPattern: this.options?.nonRecursiveWatchPattern }; } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 0e6bf5183..9d1c5ffb6 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -111,6 +111,7 @@ const configFileForOpenFiles = new FileMap(); const pendingReloads = new FileSet(); const documentRegistries = new Map(); const pendingForAllServices = new Set>(); +const projectReferenceInfo = new FileMap(); /** * For testing only: Reset the cache for services. @@ -136,7 +137,6 @@ export interface LanguageServiceDocumentContext { projectService: ProjectService | undefined; watchDirectory: ((patterns: RelativePattern[]) => void) | undefined; nonRecursiveWatchPattern: string | undefined; - projectReferenceInfo: Map; } export async function getService( @@ -156,7 +156,6 @@ export async function getService( const needAssign = !configFileForOpenFiles.has(path); let service = await getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), docContext); if (!needAssign) { - configFileForOpenFiles.set(path, tsconfigPath); return service; } @@ -246,7 +245,7 @@ export async function getServiceForTsconfig( } pendingReloads.delete(tsconfigPath); - docContext.projectReferenceInfo.delete(tsconfigPath); + projectReferenceInfo.delete(tsconfigPath); const newService = createLanguageService(tsconfigPath, workspacePath, docContext); services.set(tsconfigPathOrWorkspacePath, newService); service = await newService; @@ -409,7 +408,7 @@ async function createLanguageService( configFileName: string ) { const cached = configFileName - ? docContext.projectReferenceInfo.get(configFileName) + ? projectReferenceInfo.get(configFileName) : undefined; if (cached?.snapshotManager) { return cached.snapshotManager; @@ -571,8 +570,11 @@ async function createLanguageService( pendingProjectFileUpdate = true; } - const projectReferences = getProjectReferences(); - for (const config of projectReferences) { + if (!projectConfig.projectReferences) { + return; + } + for (const ref of projectConfig.projectReferences) { + const config = projectReferenceInfo.get(ref.path); if ( config && // handled by the respective service @@ -635,7 +637,7 @@ async function createLanguageService( let parsedConfig: ts.ParsedCommandLine; let extendedConfigPaths: Set; - const exist = tsconfigPath && docContext.projectReferenceInfo.get(tsconfigPath); + const exist = tsconfigPath && projectReferenceInfo.get(tsconfigPath); if (exist) { compilerOptions = exist.parsedCommandLine.options; parsedConfig = exist.parsedCommandLine; @@ -1016,7 +1018,6 @@ async function createLanguageService( } function ensureTsConfigInfoUpToDate(configFilePath: string) { - const projectReferenceInfo = docContext.projectReferenceInfo; const cached = projectReferenceInfo.get(configFilePath); if (cached !== undefined) { ensureProjectFileUpToDate(cached); @@ -1068,11 +1069,6 @@ async function createLanguageService( } function getParsedCommandLine(configFilePath: string) { - const info = docContext.projectReferenceInfo.get(configFilePath); - if (info) { - return info.parsedCommandLine; - } - return ensureTsConfigInfoUpToDate(configFilePath)?.parsedCommandLine; } diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index 9511de96f..a1edb7ce5 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -34,8 +34,7 @@ describe('service', () => { onProjectReloaded: undefined, projectService: undefined, nonRecursiveWatchPattern: undefined, - watchDirectory: undefined, - projectReferenceInfo: new Map() + watchDirectory: undefined }; return { virtualSystem, lsDocumentContext, rootUris }; From cc6b1c9501b39def8e23713f0ad99db7264cd8c6 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Tue, 13 Aug 2024 17:59:44 +0800 Subject: [PATCH 15/36] format --- packages/language-server/src/plugins/typescript/service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 9d1c5ffb6..c064c733c 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -407,9 +407,7 @@ async function createLanguageService( parsedCommandLine: ts.ParsedCommandLine, configFileName: string ) { - const cached = configFileName - ? projectReferenceInfo.get(configFileName) - : undefined; + const cached = configFileName ? projectReferenceInfo.get(configFileName) : undefined; if (cached?.snapshotManager) { return cached.snapshotManager; } From 08043baa4bd4f159c9ac780be4bc7d011f4888f1 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 14 Aug 2024 16:21:50 +0800 Subject: [PATCH 16/36] project-reference references another solution --- .../src/plugins/typescript/service.ts | 89 ++++++++++++------- .../project-reference/nested/expectedv2.json | 10 +++ .../project-reference/nested/imported.ts | 5 ++ .../project-reference/nested/input.svelte | 8 ++ .../nested/tsconfig_sub2.json | 7 ++ .../nested/tsconfig_sub3.json | 10 +++ .../project-reference/paths/expectedv2.json | 11 ++- .../project-reference/paths/input.svelte | 3 + .../project-reference/paths/tsconfig_sub.json | 3 +- .../fixtures/project-reference/tsconfig.json | 8 +- .../test/plugins/typescript/test-utils.ts | 28 ++++-- 11 files changed, 140 insertions(+), 42 deletions(-) create mode 100644 packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/expectedv2.json create mode 100644 packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/imported.ts create mode 100644 packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/input.svelte create mode 100644 packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/tsconfig_sub2.json create mode 100644 packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/tsconfig_sub3.json diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index c064c733c..3e6072079 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -53,9 +53,7 @@ export interface LanguageServiceContainer { onAutoImportProviderSettingsChanged(): void; onPackageJsonChange(packageJsonPath: string): void; getTsConfigSvelteOptions(): { namespace: string }; - - getProjectReferences(): TsConfigInfo[]; - + getResolvedProjectReferences(): TsConfigInfo[]; dispose(): void; } @@ -148,18 +146,29 @@ export async function getService( docContext.tsSystem.useCaseSensitiveFileNames ); + const fileExistsWithCache = (fileName: string) => { + return ( + (projectReferenceInfo.has(fileName) && !pendingReloads.has(fileName)) || + docContext.tsSystem.fileExists(fileName) + ); + }; + const tsconfigPath = configFileForOpenFiles.get(path) ?? - findTsConfigPath(path, workspaceUris, docContext.tsSystem.fileExists, getCanonicalFileName); + findTsConfigPath(path, workspaceUris, fileExistsWithCache, getCanonicalFileName); + /** + * Prevent infinite loop when the project reference is circular + */ + const triedTsConfig = new Set(); if (tsconfigPath) { const needAssign = !configFileForOpenFiles.has(path); - let service = await getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), docContext); + let service = await getConfiguredService(tsconfigPath); if (!needAssign) { return service; } - service = await findDefaultServiceForFile(path, service, docContext); + service = (await findDefaultServiceForFile(service)) ?? service; configFileForOpenFiles.set(path, service.tsconfigPath); return service; } @@ -182,30 +191,50 @@ export async function getService( docContext.tsSystem.getCurrentDirectory(), docContext ); -} -function findDefaultServiceForFile( - filePath: string, - service: LanguageServiceContainer, - docContext: LanguageServiceDocumentContext -) { - const projectReferences = service.getProjectReferences(); - if (projectReferences.length === 0 || service.snapshotManager.isProjectFile(filePath)) { - return service; + function getConfiguredService(tsconfigPath: string) { + return getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), docContext); + } + + async function findDefaultServiceForFile( + service: LanguageServiceContainer + ): Promise { + if (service.snapshotManager.isProjectFile(path)) { + return service; + } + if (triedTsConfig.has(service.tsconfigPath)) { + return; + } + + // TODO: maybe add support for ts 5.6's ancestor searching + return findDefaultFromProjectReferences(service); } - // might be possible that the file is a further nested project file - // but then we'll have to initialize a new service and the corresponding ts.Program to know - // so just skip this for now - const defaultProject = projectReferences.find((p) => p.snapshotManager.isProjectFile(filePath)); - - return defaultProject - ? getServiceForTsconfig( - defaultProject.configFilePath, - dirname(defaultProject.configFilePath), - docContext - ) - : service; + async function findDefaultFromProjectReferences(service: LanguageServiceContainer) { + const projectReferences = service.getResolvedProjectReferences(); + if (projectReferences.length === 0) { + return service; + } + + let possibleSubPaths: string[] = []; + for (const ref of projectReferences) { + if (ref.snapshotManager.isProjectFile(path)) { + return getConfiguredService(ref.configFilePath); + } + + if (ref.parsedCommandLine.projectReferences?.length) { + possibleSubPaths.push(ref.configFilePath); + } + } + + for (const ref of possibleSubPaths) { + const subService = await getConfiguredService(ref); + const defaultService = await findDefaultServiceForFile(subService); + if (defaultService) { + return defaultService; + } + } + } } export async function forAllServices( @@ -240,12 +269,12 @@ export async function getServiceForTsconfig( if (reloading || !services.has(tsconfigPathOrWorkspacePath)) { if (reloading) { Logger.log('Reloading ts service at ', tsconfigPath, ' due to config updated'); + projectReferenceInfo.delete(tsconfigPath); } else { Logger.log('Initialize new ts service at ', tsconfigPath); } pendingReloads.delete(tsconfigPath); - projectReferenceInfo.delete(tsconfigPath); const newService = createLanguageService(tsconfigPath, workspacePath, docContext); services.set(tsconfigPathOrWorkspacePath, newService); service = await newService; @@ -399,7 +428,7 @@ async function createLanguageService( onAutoImportProviderSettingsChanged, onPackageJsonChange, getTsConfigSvelteOptions, - getProjectReferences, + getResolvedProjectReferences, dispose }; @@ -1078,7 +1107,7 @@ async function createLanguageService( } } - function getProjectReferences(): TsConfigInfo[] { + function getResolvedProjectReferences(): TsConfigInfo[] { if (!tsconfigPath || !projectConfig.projectReferences) { return []; } diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/expectedv2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/expectedv2.json new file mode 100644 index 000000000..ddef317a6 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/expectedv2.json @@ -0,0 +1,10 @@ +[ + { + "range": { "start": { "line": 6, "character": 8 }, "end": { "line": 6, "character": 9 } }, + "severity": 1, + "source": "ts", + "message": "Type 'string' is not assignable to type 'number'.", + "code": 2322, + "tags": [] + } +] diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/imported.ts b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/imported.ts new file mode 100644 index 000000000..bc8481bbb --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/imported.ts @@ -0,0 +1,5 @@ +/** + * + * @param cb callback because if the module resolution failed there will be a noImplicitAny error + */ +export function hi(cb: (num: number) => string) {} diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/input.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/input.svelte new file mode 100644 index 000000000..3b505cd2c --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/tsconfig_sub2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/tsconfig_sub2.json new file mode 100644 index 000000000..310ef4967 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/tsconfig_sub2.json @@ -0,0 +1,7 @@ +{ + "include": [], + "compilerOptions": { + "composite": true + }, + "references": [{ "path": "./tsconfig_sub3.json" }] +} diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/tsconfig_sub3.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/tsconfig_sub3.json new file mode 100644 index 000000000..f96815577 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/nested/tsconfig_sub3.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "strict": true, + "paths": { + "hi2": ["./imported.ts"] + }, + "types": [] + } +} diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json index fe51488c7..3d707cc47 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/expectedv2.json @@ -1 +1,10 @@ -[] +[ + { + "range": { "start": { "line": 6, "character": 4 }, "end": { "line": 6, "character": 5 } }, + "severity": 1, + "source": "ts", + "message": "Type 'string' is not assignable to type 'number'.", + "code": 2322, + "tags": [] + } +] diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/input.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/input.svelte index fb6f5dfef..5e19507ef 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/input.svelte +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/input.svelte @@ -2,4 +2,7 @@ import { hi } from 'hi'; hi((num) => num.toString()) + +// project reference redirect skip check so put an error here +let a: number = 'a'; a; diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/tsconfig_sub.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/tsconfig_sub.json index 521061053..384062fc0 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/tsconfig_sub.json +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/paths/tsconfig_sub.json @@ -5,6 +5,7 @@ "strict": true, "paths": { "hi": ["./imported.ts"] - } + }, + "types": [] } } diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/tsconfig.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/tsconfig.json index 83937370e..59b56bf8f 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/tsconfig.json +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/project-reference/tsconfig.json @@ -3,9 +3,9 @@ "references": [ { "path": "./paths/tsconfig_sub.json" + }, + { + "path": "./nested/tsconfig_sub2.json" } - ], - "compilerOptions": { - "types": [] - } + ] } diff --git a/packages/language-server/test/plugins/typescript/test-utils.ts b/packages/language-server/test/plugins/typescript/test-utils.ts index 4810e99c1..c738bbd17 100644 --- a/packages/language-server/test/plugins/typescript/test-utils.ts +++ b/packages/language-server/test/plugins/typescript/test-utils.ts @@ -278,23 +278,35 @@ export async function createJsonSnapshotFormatter(dir: string) { }); } -export function serviceWarmup(suite: Mocha.Suite, testDir: string, rootUri = pathToUrl(testDir)) { +export function serviceWarmup( + suite: Mocha.Suite, + testDir: string, + rootUri = pathToUrl(testDir), + tsconfigPath: string | undefined = undefined +) { const defaultTimeout = suite.timeout(); // allow to set a higher timeout for slow machines from cli flag const warmupTimeout = Math.max(defaultTimeout, 5_000); suite.timeout(warmupTimeout); - before(async () => { + before(() => warmup(tsconfigPath)); + + suite.timeout(defaultTimeout); + + async function warmup(configFilePath: string | undefined = undefined) { const start = Date.now(); console.log('Warming up language service...'); const docManager = new DocumentManager( (textDocument) => new Document(textDocument.uri, textDocument.text) ); + + const options = configFilePath ? { tsconfigPath: configFilePath } : undefined; const lsAndTsDocResolver = new LSAndTSDocResolver( docManager, [rootUri], - new LSConfigManager() + new LSConfigManager(), + options ); const filePath = join(testDir, 'DoesNotMater.svelte'); @@ -303,12 +315,16 @@ export function serviceWarmup(suite: Mocha.Suite, testDir: string, rootUri = pat text: ts.sys.readFile(filePath) || '' }); + const ls = await lsAndTsDocResolver.getTSService(filePath); await lsAndTsDocResolver.getLSAndTSDoc(document); + const projectReferences = ls.getResolvedProjectReferences(); - console.log(`Service warming up done in ${Date.now() - start}ms`); - }); + if (projectReferences.length) { + await Promise.all(projectReferences.map((ref) => warmup(ref.configFilePath))); + } - suite.timeout(defaultTimeout); + console.log(`Service warming up done in ${Date.now() - start}ms`); + } } export function recursiveServiceWarmup( From d9a5fca7602b2a2e698490dd1c508f0cad7b9ae8 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 14 Aug 2024 17:30:20 +0800 Subject: [PATCH 17/36] clear projectReferenceInfo as well otherwise the snapshotManager will be reused --- packages/language-server/src/plugins/typescript/service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 3e6072079..3f6b11484 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -118,6 +118,7 @@ const projectReferenceInfo = new FileMap(); */ export function __resetCache() { services.clear(); + projectReferenceInfo.clear(); serviceSizeMap.clear(); configFileForOpenFiles.clear(); } From 36d222c7ab9d1efb5019fe52637409e0ede01f89 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 14 Aug 2024 17:56:45 +0800 Subject: [PATCH 18/36] breaking: force less tsconfig --- .../src/plugins/typescript/service.ts | 66 +++++++++++-------- .../diagnostics/fixtures/tsconfig.json | 2 + .../test/plugins/typescript/service.test.ts | 7 +- .../testfiles/diagnostics/tsconfig.json | 2 + .../typescript/testfiles/tsconfig.json | 2 + 5 files changed, 47 insertions(+), 32 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 3f6b11484..fda4145f6 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -158,18 +158,18 @@ export async function getService( configFileForOpenFiles.get(path) ?? findTsConfigPath(path, workspaceUris, fileExistsWithCache, getCanonicalFileName); - /** - * Prevent infinite loop when the project reference is circular - */ - const triedTsConfig = new Set(); if (tsconfigPath) { + /** + * Prevent infinite loop when the project reference is circular + */ + const triedTsConfig = new Set(); const needAssign = !configFileForOpenFiles.has(path); - let service = await getConfiguredService(tsconfigPath); + let service = await getConfiguredService(tsconfigPath, triedTsConfig); if (!needAssign) { return service; } - service = (await findDefaultServiceForFile(service)) ?? service; + service = (await findDefaultServiceForFile(service, triedTsConfig)) ?? service; configFileForOpenFiles.set(path, service.tsconfigPath); return service; } @@ -193,12 +193,13 @@ export async function getService( docContext ); - function getConfiguredService(tsconfigPath: string) { + function getConfiguredService(tsconfigPath: string, triedTsConfig: Set) { return getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), docContext); } async function findDefaultServiceForFile( - service: LanguageServiceContainer + service: LanguageServiceContainer, + triedTsConfig: Set ): Promise { if (service.snapshotManager.isProjectFile(path)) { return service; @@ -208,10 +209,13 @@ export async function getService( } // TODO: maybe add support for ts 5.6's ancestor searching - return findDefaultFromProjectReferences(service); + return findDefaultFromProjectReferences(service, triedTsConfig); } - async function findDefaultFromProjectReferences(service: LanguageServiceContainer) { + async function findDefaultFromProjectReferences( + service: LanguageServiceContainer, + triedTsConfig: Set + ) { const projectReferences = service.getResolvedProjectReferences(); if (projectReferences.length === 0) { return service; @@ -220,7 +224,7 @@ export async function getService( let possibleSubPaths: string[] = []; for (const ref of projectReferences) { if (ref.snapshotManager.isProjectFile(path)) { - return getConfiguredService(ref.configFilePath); + return getConfiguredService(ref.configFilePath, triedTsConfig); } if (ref.parsedCommandLine.projectReferences?.length) { @@ -229,8 +233,8 @@ export async function getService( } for (const ref of possibleSubPaths) { - const subService = await getConfiguredService(ref); - const defaultService = await findDefaultServiceForFile(subService); + const subService = await getConfiguredService(ref, triedTsConfig); + const defaultService = await findDefaultServiceForFile(subService, triedTsConfig); if (defaultService) { return defaultService; } @@ -297,8 +301,9 @@ async function createLanguageService( ): Promise { const { tsSystem } = docContext; + const configErrors: ts.Diagnostic[] = []; const projectConfig = getParsedConfig(); - const { options: compilerOptions, raw, errors: configErrors } = projectConfig; + const { options: compilerOptions, raw } = projectConfig; const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); watchWildCardDirectories(projectConfig); @@ -358,7 +363,6 @@ async function createLanguageService( let languageServiceReducedMode = false; let projectVersion = 0; let dirty = false; - let pendingProjectFileUpdate = false; const host: ts.LanguageServiceHost = { log: (message) => Logger.debug(`[ts] ${message}`), @@ -482,10 +486,8 @@ async function createLanguageService( } function getService(skipSynchronize?: boolean) { - if (pendingProjectFileUpdate) { - updateProjectFiles(); - pendingProjectFileUpdate = false; - } + updateProjectFiles(); + if (!skipSynchronize) { updateIfDirty(); } @@ -595,7 +597,10 @@ async function createLanguageService( function scheduleProjectFileUpdate(watcherNewFiles: string[]): void { if (!snapshotManager.areIgnoredFromNewFileWatch(watcherNewFiles)) { scheduleUpdate(); - pendingProjectFileUpdate = true; + const info = projectReferenceInfo.get(tsconfigPath); + if (info) { + info.pendingProjectFileUpdate = true; + } } if (!projectConfig.projectReferences) { @@ -616,8 +621,12 @@ async function createLanguageService( } function updateProjectFiles(): void { + const info = projectReferenceInfo.get(tsconfigPath); + if (!info || !info.pendingProjectFileUpdate) { + return; + } const projectFileCountBefore = snapshotManager.getProjectFileNames().length; - snapshotManager.updateProjectFiles(); + ensureProjectFileUpToDate(info); const projectFileCountAfter = snapshotManager.getProjectFileNames().length; if (projectFileCountAfter > projectFileCountBefore) { @@ -665,11 +674,16 @@ async function createLanguageService( let parsedConfig: ts.ParsedCommandLine; let extendedConfigPaths: Set; - const exist = tsconfigPath && projectReferenceInfo.get(tsconfigPath); - if (exist) { - compilerOptions = exist.parsedCommandLine.options; - parsedConfig = exist.parsedCommandLine; - extendedConfigPaths = exist.extendedConfigPaths ?? new Set(); + if (tsconfigPath) { + const info = ensureTsConfigInfoUpToDate(tsconfigPath); + // tsconfig is either found from file-system or passed from svelte-check + // so this is already be validated to exist + if (!info) { + throw new Error('Failed to get tsconfig: ' + tsconfigPath); + } + compilerOptions = info.parsedCommandLine.options; + parsedConfig = info.parsedCommandLine; + extendedConfigPaths = info.extendedConfigPaths ?? new Set(); } else { const config = parseDefaultCompilerOptions(); compilerOptions = config.compilerOptions; diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json index dd94fecae..da66f1d47 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/tsconfig.json @@ -1,6 +1,8 @@ { "compilerOptions": { "strict": true, + "allowJs": true, + "target": "ESNext", /** This is actually not needed, but makes the tests faster because TS does not look up other types. diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index a1edb7ce5..ee195b079 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -64,16 +64,11 @@ describe('service', () => { delete ls.compilerOptions.configFilePath; assert.deepStrictEqual(ls.compilerOptions, { - allowJs: true, allowNonTsExtensions: true, checkJs: true, strict: true, - declaration: false, module: ts.ModuleKind.ESNext, - moduleResolution: ts.ModuleResolutionKind.Node10, - noEmit: true, - skipLibCheck: true, - target: ts.ScriptTarget.ESNext + moduleResolution: ts.ModuleResolutionKind.Node10 }); }); diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/tsconfig.json b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/tsconfig.json index 89dd24236..8cd68407b 100644 --- a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/tsconfig.json +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/tsconfig.json @@ -1,5 +1,7 @@ { "compilerOptions": { + "allowJs": true, + "target": "ESNext", "strict": true, /** This is actually not needed, but makes the tests faster diff --git a/packages/language-server/test/plugins/typescript/testfiles/tsconfig.json b/packages/language-server/test/plugins/typescript/testfiles/tsconfig.json index 63ec98505..03afba4d9 100644 --- a/packages/language-server/test/plugins/typescript/testfiles/tsconfig.json +++ b/packages/language-server/test/plugins/typescript/testfiles/tsconfig.json @@ -1,5 +1,7 @@ { "compilerOptions": { + "allowJs": true, + "target": "ESNext", "strict": true, /** This is actually not needed, but makes the tests faster From 7d0f346598b4e2fe99400fcabcefa3f2344fee0f Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 16 Aug 2024 09:23:18 +0800 Subject: [PATCH 19/36] avoid creating new service when file is in multiple services --- .../plugins/typescript/LSAndTSDocResolver.ts | 59 ++++++++++--------- .../plugins/typescript/TypeScriptPlugin.ts | 9 ++- .../features/CallHierarchyProvider.ts | 11 ++-- .../features/CodeActionsProvider.ts | 8 +-- .../FindComponentReferencesProvider.ts | 7 ++- .../features/FindFileReferencesProvider.ts | 11 ++-- .../features/FindReferencesProvider.ts | 8 +-- .../features/ImplementationProvider.ts | 4 +- .../typescript/features/InlayHintProvider.ts | 4 +- .../typescript/features/RenameProvider.ts | 6 +- .../features/TypeDefinitionProvider.ts | 4 +- .../src/plugins/typescript/features/utils.ts | 27 +++++++-- .../src/plugins/typescript/service.ts | 30 ++++++---- .../features/DiagnosticsProvider.test.ts | 2 +- .../features/FindReferencesProvider.test.ts | 6 +- .../typescript/features/HoverProvider.test.ts | 6 +- .../features/ImplemenationProvider.test.ts | 2 +- .../features/TypeDefinitionProvider.test.ts | 2 +- .../features/UpdateImportsProvider.test.ts | 12 +++- .../inlayHints/fixtures/tsconfig.json | 12 ++++ .../test/plugins/typescript/test-utils.ts | 21 +++---- 21 files changed, 153 insertions(+), 98 deletions(-) create mode 100644 packages/language-server/test/plugins/typescript/features/inlayHints/fixtures/tsconfig.json diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index 3843e8a89..a80d3cddb 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -50,14 +50,10 @@ export class LSAndTSDocResolver { private readonly configManager: LSConfigManager, private readonly options?: LSAndTSDocResolverOptions ) { - const handleDocumentChange = (document: Document) => { - // This refreshes the document in the ts language service - this.getSnapshot(document); - }; docManager.on( 'documentChange', debounceSameArg( - handleDocumentChange, + this.updateSnapshot.bind(this), (newDoc, prevDoc) => newDoc.uri === prevDoc?.uri, 1000 ) @@ -68,7 +64,11 @@ export class LSAndTSDocResolver { // where multiple files and their dependencies // being loaded in a short period of times docManager.on('documentOpen', (document) => { - handleDocumentChange(document); + if (document.openedByClient) { + this.getOrCreateSnapshot(document); + } else { + this.updateSnapshot(document); + } docManager.lockDocument(document.uri); }); @@ -151,18 +151,20 @@ export class LSAndTSDocResolver { private lsDocumentContext: LanguageServiceDocumentContext; private readonly watchedDirectories: FileSet; - async getLSForPath(path: string) { - return (await this.getTSService(path)).getService(); - } - async getLSAndTSDoc(document: Document): Promise<{ tsDoc: SvelteDocumentSnapshot; lang: ts.LanguageService; userPreferences: ts.UserPreferences; + tsconfigPath: string; }> { const { tsDoc, lsContainer, userPreferences } = await this.getLSAndTSDocWorker(document); - return { tsDoc, lang: lsContainer.getService(), userPreferences }; + return { + tsDoc, + lang: lsContainer.getService(), + userPreferences, + tsconfigPath: lsContainer.tsconfigPath + }; } /** @@ -181,7 +183,7 @@ export class LSAndTSDocResolver { private async getLSAndTSDocWorker(document: Document) { const lsContainer = await this.getTSService(document.getFilePath() || ''); - const tsDoc = await this.getSnapshot(document); + const tsDoc = await this.getOrCreateSnapshot(document); const userPreferences = this.getUserPreferences(tsDoc); return { tsDoc, lsContainer, userPreferences }; @@ -192,13 +194,21 @@ export class LSAndTSDocResolver { * the ts service it primarily belongs into. * The update is mirrored in all other services, too. */ - async getSnapshot(document: Document): Promise; - async getSnapshot(pathOrDoc: string | Document): Promise; - async getSnapshot(pathOrDoc: string | Document) { + async getOrCreateSnapshot(document: Document): Promise; + async getOrCreateSnapshot(pathOrDoc: string | Document): Promise; + async getOrCreateSnapshot(pathOrDoc: string | Document) { const filePath = typeof pathOrDoc === 'string' ? pathOrDoc : pathOrDoc.getFilePath() || ''; const tsService = await this.getTSService(filePath); return tsService.updateSnapshot(pathOrDoc); } + private async updateSnapshot(document: Document) { + const filePath = document.getFilePath(); + if (!filePath) { + return; + } + // ensure no new service is created + await this.updateExistingFile(filePath, (service) => service.updateSnapshot(document)); + } /** * Updates snapshot path in all existing ts services and retrieves snapshot @@ -217,7 +227,7 @@ export class LSAndTSDocResolver { }); } else { // This may not be a file but a directory, still try - await this.getSnapshot(newPath); + await this.getOrCreateSnapshot(newPath); } } @@ -280,20 +290,9 @@ export class LSAndTSDocResolver { }); } - /** - * @internal Public for tests only - */ - async getSnapshotManager(filePath: string): Promise { - return (await this.getTSService(filePath)).snapshotManager; - } - async getTSService(filePath?: string): Promise { if (this.options?.tsconfigPath) { - return getServiceForTsconfig( - this.options?.tsconfigPath, - dirname(this.options.tsconfigPath), - this.lsDocumentContext - ); + return this.getTSServiceByConfigPath(this.options.tsconfigPath); } if (!filePath) { throw new Error('Cannot call getTSService without filePath and without tsconfigPath'); @@ -301,6 +300,10 @@ export class LSAndTSDocResolver { return getService(filePath, this.workspaceUris, this.lsDocumentContext); } + async getTSServiceByConfigPath(tsconfigPath: string): Promise { + return getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), this.lsDocumentContext); + } + private getUserPreferences(tsDoc: DocumentSnapshot): ts.UserPreferences { const configLang = tsDoc.scriptKind === ts.ScriptKind.TS || tsDoc.scriptKind === ts.ScriptKind.TSX diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index bc97f6175..2b91ac323 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -168,7 +168,10 @@ export class TypeScriptPlugin this.completionProvider, configManager ); - this.updateImportsProvider = new UpdateImportsProviderImpl(this.lsAndTsDocResolver); + this.updateImportsProvider = new UpdateImportsProviderImpl( + this.lsAndTsDocResolver, + ts.sys.useCaseSensitiveFileNames + ); this.diagnosticsProvider = new DiagnosticsProviderImpl( this.lsAndTsDocResolver, configManager @@ -383,7 +386,7 @@ export class TypeScriptPlugin } async getDefinitions(document: Document, position: Position): Promise { - const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { lang, tsDoc, tsconfigPath } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); const defs = lang.getDefinitionAndBoundSpan( tsDoc.filePath, @@ -394,7 +397,7 @@ export class TypeScriptPlugin return []; } - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); snapshots.set(tsDoc.filePath, tsDoc); const result = await Promise.all( diff --git a/packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts b/packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts index b58e797a2..136906e27 100644 --- a/packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts @@ -41,7 +41,7 @@ export class CallHierarchyProviderImpl implements CallHierarchyProvider { position: Position, cancellationToken?: CancellationToken ): Promise { - const { lang, tsDoc } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { lang, tsDoc, tsconfigPath } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); if (cancellationToken?.isCancellationRequested) { return null; @@ -52,7 +52,7 @@ export class CallHierarchyProviderImpl implements CallHierarchyProvider { const itemsArray = Array.isArray(items) ? items : items ? [items] : []; - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); snapshots.set(tsDoc.filePath, tsDoc); const program = lang.getProgram(); @@ -251,8 +251,9 @@ export class CallHierarchyProviderImpl implements CallHierarchyProvider { return null; } - const lang = await this.lsAndTsDocResolver.getLSForPath(filePath); - const tsDoc = await this.lsAndTsDocResolver.getSnapshot(filePath); + const lsContainer = await this.lsAndTsDocResolver.getTSService(filePath); + const lang = lsContainer.getService(); + const tsDoc = await this.lsAndTsDocResolver.getOrCreateSnapshot(filePath); if (cancellationToken?.isCancellationRequested) { return null; @@ -260,7 +261,7 @@ export class CallHierarchyProviderImpl implements CallHierarchyProvider { const program = lang.getProgram(); - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, lsContainer.tsconfigPath); snapshots.set(tsDoc.filePath, tsDoc); const isComponentModulePosition = diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts index 287f21664..8442bab74 100644 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts @@ -156,7 +156,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { return codeAction; } - const { lang, tsDoc, userPreferences } = + const { lang, tsDoc, userPreferences, tsconfigPath } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); if (cancellationToken?.isCancellationRequested) { return codeAction; @@ -218,7 +218,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { await this.lsAndTsDocResolver.deleteSnapshot(virtualDocPath); } - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); const fixActions: ts.CodeFixAction[] = [ { fixName: codeAction.data.fixName, @@ -553,7 +553,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { context: CodeActionContext, cancellationToken: CancellationToken | undefined ) { - const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document); + const { lang, tsDoc, userPreferences, tsconfigPath } = await this.getLSAndTSDoc(document); if (cancellationToken?.isCancellationRequested) { return []; @@ -613,7 +613,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { ); } - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); snapshots.set(tsDoc.filePath, tsDoc); const codeActionsPromises = codeFixes.map(async (fix) => { diff --git a/packages/language-server/src/plugins/typescript/features/FindComponentReferencesProvider.ts b/packages/language-server/src/plugins/typescript/features/FindComponentReferencesProvider.ts index a0bcca91d..b47926269 100644 --- a/packages/language-server/src/plugins/typescript/features/FindComponentReferencesProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/FindComponentReferencesProvider.ts @@ -20,8 +20,9 @@ export class FindComponentReferencesProviderImpl implements FindComponentReferen return null; } - const lang = await this.lsAndTsDocResolver.getLSForPath(fileName); - const tsDoc = await this.lsAndTsDocResolver.getSnapshot(fileName); + const lsContainer = await this.lsAndTsDocResolver.getTSService(fileName); + const lang = lsContainer.getService(); + const tsDoc = await this.lsAndTsDocResolver.getOrCreateSnapshot(fileName); if (!(tsDoc instanceof SvelteDocumentSnapshot)) { return null; } @@ -34,7 +35,7 @@ export class FindComponentReferencesProviderImpl implements FindComponentReferen return null; } - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, lsContainer.tsconfigPath); snapshots.set(tsDoc.filePath, tsDoc); const locations = await Promise.all( diff --git a/packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts b/packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts index c9ee7d213..0969aae4a 100644 --- a/packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts @@ -13,7 +13,8 @@ export class FindFileReferencesProviderImpl implements FileReferencesProvider { const u = URI.parse(uri); const fileName = u.fsPath; - const lang = await this.getLSForPath(fileName); + const lsContainer = await this.lsAndTsDocResolver.getTSService(fileName); + const lang = lsContainer.getService(); const tsDoc = await this.getSnapshotForPath(fileName); const references = lang.getFileReferences(fileName); @@ -22,7 +23,7 @@ export class FindFileReferencesProviderImpl implements FileReferencesProvider { return null; } - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, lsContainer.tsconfigPath); snapshots.set(tsDoc.filePath, tsDoc); const locations = await Promise.all( @@ -40,11 +41,7 @@ export class FindFileReferencesProviderImpl implements FileReferencesProvider { return locations.filter(hasNonZeroRange); } - private async getLSForPath(path: string) { - return this.lsAndTsDocResolver.getLSForPath(path); - } - private async getSnapshotForPath(path: string) { - return this.lsAndTsDocResolver.getSnapshot(path); + return this.lsAndTsDocResolver.getOrCreateSnapshot(path); } } diff --git a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts b/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts index b609d5c5d..358d835d7 100644 --- a/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/FindReferencesProvider.ts @@ -1,7 +1,7 @@ import ts from 'typescript'; import { CancellationToken, Location, Position, ReferenceContext } from 'vscode-languageserver'; import { Document } from '../../../lib/documents'; -import { flatten, isNotNullOrUndefined, pathToUrl } from '../../../utils'; +import { flatten, isNotNullOrUndefined, normalizePath, pathToUrl } from '../../../utils'; import { FindComponentReferencesProvider, FindReferencesProvider } from '../../interfaces'; import { SvelteDocumentSnapshot } from '../DocumentSnapshot'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; @@ -38,7 +38,7 @@ export class FindReferencesProviderImpl implements FindReferencesProvider { return this.componentReferencesProvider.findComponentReferences(document.uri); } - const { lang, tsDoc } = await this.getLSAndTSDoc(document); + const { lang, tsDoc, tsconfigPath } = await this.getLSAndTSDoc(document); if (cancellationToken?.isCancellationRequested) { return null; } @@ -52,7 +52,7 @@ export class FindReferencesProviderImpl implements FindReferencesProvider { return null; } - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); snapshots.set(tsDoc.filePath, tsDoc); if (rawReferences.some((ref) => ref.definition.kind === ts.ScriptElementKind.alias)) { @@ -124,7 +124,7 @@ export class FindReferencesProviderImpl implements FindReferencesProvider { let storeReferences: ts.ReferencedSymbolEntry[] = []; const storeReference = references.find( (ref) => - ref.fileName === tsDoc.filePath && + normalizePath(ref.fileName) === tsDoc.filePath && isTextSpanInGeneratedCode(tsDoc.getFullText(), ref.textSpan) && is$storeVariableIn$storeDeclaration(tsDoc.getFullText(), ref.textSpan.start) ); diff --git a/packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts b/packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts index e4a91c1bb..5cbf13b0b 100644 --- a/packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts @@ -18,7 +18,7 @@ export class ImplementationProviderImpl implements ImplementationProvider { position: Position, cancellationToken?: CancellationToken ): Promise { - const { tsDoc, lang } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { tsDoc, lang, tsconfigPath } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); if (cancellationToken?.isCancellationRequested) { return null; @@ -27,7 +27,7 @@ export class ImplementationProviderImpl implements ImplementationProvider { const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); const implementations = lang.getImplementationAtPosition(tsDoc.filePath, offset); - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); snapshots.set(tsDoc.filePath, tsDoc); if (!implementations) { diff --git a/packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts b/packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts index 9ca919900..7c48664e5 100644 --- a/packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts @@ -41,7 +41,7 @@ export class InlayHintProviderImpl implements InlayHintProvider { return null; } - const { tsDoc, lang } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { tsDoc, lang, tsconfigPath } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); const inlayHints = lang.provideInlayHints( tsDoc.filePath, @@ -59,7 +59,7 @@ export class InlayHintProviderImpl implements InlayHintProvider { const renderFunctionReturnTypeLocation = renderFunction && this.getTypeAnnotationPosition(renderFunction); - const snapshotMap = new SnapshotMap(this.lsAndTsDocResolver); + const snapshotMap = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); snapshotMap.set(tsDoc.filePath, tsDoc); const convertPromises = inlayHints diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts index 281ae659f..3e67a18dc 100644 --- a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts @@ -65,7 +65,7 @@ export class RenameProviderImpl implements RenameProvider { position: Position, newName: string ): Promise { - const { lang, tsDoc } = await this.getLSAndTSDoc(document); + const { lang, tsDoc, tsconfigPath } = await this.getLSAndTSDoc(document); const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); @@ -85,7 +85,7 @@ export class RenameProviderImpl implements RenameProvider { return null; } - const docs = new SnapshotMap(this.lsAndTsDocResolver); + const docs = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); docs.set(tsDoc.filePath, tsDoc); let convertedRenameLocations: TsRenameLocation[] = await this.mapAndFilterRenameLocations( @@ -536,7 +536,7 @@ export class RenameProviderImpl implements RenameProvider { } private getSnapshot(filePath: string) { - return this.lsAndTsDocResolver.getSnapshot(filePath); + return this.lsAndTsDocResolver.getOrCreateSnapshot(filePath); } private checkShortHandBindingOrSlotLetLocation( diff --git a/packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts b/packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts index 93674fd9e..1cd619eaf 100644 --- a/packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts @@ -10,11 +10,11 @@ export class TypeDefinitionProviderImpl implements TypeDefinitionProvider { constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} async getTypeDefinition(document: Document, position: Position): Promise { - const { tsDoc, lang } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { tsDoc, lang, tsconfigPath } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); const typeDefs = lang.getTypeDefinitionAtPosition(tsDoc.filePath, offset); - const snapshots = new SnapshotMap(this.lsAndTsDocResolver); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); snapshots.set(tsDoc.filePath, tsDoc); if (!typeDefs) { diff --git a/packages/language-server/src/plugins/typescript/features/utils.ts b/packages/language-server/src/plugins/typescript/features/utils.ts index f8ee48a21..c43072f10 100644 --- a/packages/language-server/src/plugins/typescript/features/utils.ts +++ b/packages/language-server/src/plugins/typescript/features/utils.ts @@ -12,6 +12,7 @@ import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; import { or } from '../../../utils'; import { FileMap } from '../../../lib/documents/fileCollection'; import { LSConfig } from '../../../ls-config'; +import { LanguageServiceContainer } from '../service'; type NodePredicate = (node: ts.Node) => boolean; @@ -144,7 +145,11 @@ export function getStoreOffsetOf$storeDeclaration(text: string, $storeVarStart: export class SnapshotMap { private map = new FileMap(); - constructor(private resolver: LSAndTSDocResolver) {} + private sourceLs: LanguageServiceContainer | undefined; + constructor( + private resolver: LSAndTSDocResolver, + private readonly tsconfigPath: string + ) {} set(fileName: string, snapshot: DocumentSnapshot) { this.map.set(fileName, snapshot); @@ -156,12 +161,22 @@ export class SnapshotMap { async retrieve(fileName: string) { let snapshot = this.get(fileName); - if (!snapshot) { - const snap = await this.resolver.getSnapshot(fileName); - this.set(fileName, snap); - snapshot = snap; + if (snapshot) { + return snapshot; } - return snapshot; + + if (!this.sourceLs) { + this.sourceLs = await this.resolver.getTSServiceByConfigPath(this.tsconfigPath); + } + + const snap = + this.sourceLs.snapshotManager.get(fileName) ?? + // should not happen in most cases, + // the file should be in the project otherwise why would we know about it + (await this.resolver.getOrCreateSnapshot(fileName)); + + this.set(fileName, snap); + return snap; } } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index fda4145f6..43b99742b 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -39,6 +39,7 @@ export interface LanguageServiceContainer { deleteSnapshot(filePath: string): void; invalidateModuleCache(filePath: string[]): void; scheduleProjectFileUpdate(watcherNewFiles: string[]): void; + ensureProjectFileUpdates(): void; updateTsOrJsFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void; /** * Checks if a file is present in the project. @@ -164,7 +165,7 @@ export async function getService( */ const triedTsConfig = new Set(); const needAssign = !configFileForOpenFiles.has(path); - let service = await getConfiguredService(tsconfigPath, triedTsConfig); + let service = await getConfiguredService(tsconfigPath); if (!needAssign) { return service; } @@ -193,7 +194,7 @@ export async function getService( docContext ); - function getConfiguredService(tsconfigPath: string, triedTsConfig: Set) { + function getConfiguredService(tsconfigPath: string) { return getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), docContext); } @@ -201,6 +202,7 @@ export async function getService( service: LanguageServiceContainer, triedTsConfig: Set ): Promise { + service.ensureProjectFileUpdates(); if (service.snapshotManager.isProjectFile(path)) { return service; } @@ -218,13 +220,13 @@ export async function getService( ) { const projectReferences = service.getResolvedProjectReferences(); if (projectReferences.length === 0) { - return service; + return undefined; } let possibleSubPaths: string[] = []; for (const ref of projectReferences) { if (ref.snapshotManager.isProjectFile(path)) { - return getConfiguredService(ref.configFilePath, triedTsConfig); + return getConfiguredService(ref.configFilePath); } if (ref.parsedCommandLine.projectReferences?.length) { @@ -233,7 +235,7 @@ export async function getService( } for (const ref of possibleSubPaths) { - const subService = await getConfiguredService(ref, triedTsConfig); + const subService = await getConfiguredService(ref); const defaultService = await findDefaultServiceForFile(subService, triedTsConfig); if (defaultService) { return defaultService; @@ -266,6 +268,9 @@ export async function getServiceForTsconfig( workspacePath: string, docContext: LanguageServiceDocumentContext ): Promise { + if (tsconfigPath) { + tsconfigPath = normalizePath(tsconfigPath); + } const tsconfigPathOrWorkspacePath = tsconfigPath || workspacePath; const reloading = pendingReloads.has(tsconfigPath); @@ -362,7 +367,7 @@ async function createLanguageService( let languageServiceReducedMode = false; let projectVersion = 0; - let dirty = false; + let dirty = projectConfig.fileNames.length > 0; const host: ts.LanguageServiceHost = { log: (message) => Logger.debug(`[ts] ${message}`), @@ -426,6 +431,7 @@ async function createLanguageService( deleteSnapshot, scheduleProjectFileUpdate, updateTsOrJsFile, + ensureProjectFileUpdates, hasFile, fileBelongsToProject, snapshotManager, @@ -452,7 +458,7 @@ async function createLanguageService( parsedCommandLine.raw, workspacePath, tsSystem, - parsedCommandLine.fileNames, + parsedCommandLine.fileNames.map(normalizePath), parsedCommandLine.wildcardDirectories ); } @@ -486,7 +492,7 @@ async function createLanguageService( } function getService(skipSynchronize?: boolean) { - updateProjectFiles(); + ensureProjectFileUpdates(); if (!skipSynchronize) { updateIfDirty(); @@ -620,13 +626,13 @@ async function createLanguageService( } } - function updateProjectFiles(): void { + function ensureProjectFileUpdates(): void { const info = projectReferenceInfo.get(tsconfigPath); if (!info || !info.pendingProjectFileUpdate) { return; } const projectFileCountBefore = snapshotManager.getProjectFileNames().length; - ensureProjectFileUpToDate(info); + ensureFilesForConfigUpdates(info); const projectFileCountAfter = snapshotManager.getProjectFileNames().length; if (projectFileCountAfter > projectFileCountBefore) { @@ -1062,7 +1068,7 @@ async function createLanguageService( function ensureTsConfigInfoUpToDate(configFilePath: string) { const cached = projectReferenceInfo.get(configFilePath); if (cached !== undefined) { - ensureProjectFileUpToDate(cached); + ensureFilesForConfigUpdates(cached); return cached; } @@ -1114,7 +1120,7 @@ async function createLanguageService( return ensureTsConfigInfoUpToDate(configFilePath)?.parsedCommandLine; } - function ensureProjectFileUpToDate(info: TsConfigInfo | null) { + function ensureFilesForConfigUpdates(info: TsConfigInfo | null) { if (info?.pendingProjectFileUpdate) { info.pendingProjectFileUpdate = false; info.snapshotManager.updateProjectFiles(); diff --git a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts index 92601459f..dbccee6e5 100644 --- a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts @@ -98,7 +98,7 @@ describe('DiagnosticsProvider', function () { ); const newFilePath = normalizePath(path.join(testDir, 'empty-export.ts')) || ''; - await lsAndTsDocResolver.getSnapshot(newFilePath); + await lsAndTsDocResolver.getOrCreateSnapshot(newFilePath); const diagnostics1 = await plugin.getDiagnostics(document); assert.deepStrictEqual( diff --git a/packages/language-server/test/plugins/typescript/features/FindReferencesProvider.test.ts b/packages/language-server/test/plugins/typescript/features/FindReferencesProvider.test.ts index ecd5d4f5b..12509c1c6 100644 --- a/packages/language-server/test/plugins/typescript/features/FindReferencesProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/FindReferencesProvider.test.ts @@ -31,7 +31,11 @@ describe('FindReferencesProvider', function () { (textDocument) => new Document(textDocument.uri, textDocument.text) ); const lsConfigManager = new LSConfigManager(); - const lsAndTsDocResolver = new LSAndTSDocResolver(docManager, [testDir], lsConfigManager); + const lsAndTsDocResolver = new LSAndTSDocResolver( + docManager, + [pathToUrl(testDir)], + lsConfigManager + ); const provider = new FindReferencesProviderImpl( lsAndTsDocResolver, new FindComponentReferencesProviderImpl(lsAndTsDocResolver) diff --git a/packages/language-server/test/plugins/typescript/features/HoverProvider.test.ts b/packages/language-server/test/plugins/typescript/features/HoverProvider.test.ts index 17b6d8c35..49e146108 100644 --- a/packages/language-server/test/plugins/typescript/features/HoverProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/HoverProvider.test.ts @@ -25,7 +25,11 @@ describe('HoverProvider', function () { (textDocument) => new Document(textDocument.uri, textDocument.text) ); const lsConfigManager = new LSConfigManager(); - const lsAndTsDocResolver = new LSAndTSDocResolver(docManager, [testDir], lsConfigManager); + const lsAndTsDocResolver = new LSAndTSDocResolver( + docManager, + [pathToUrl(testDir)], + lsConfigManager + ); const provider = new HoverProviderImpl(lsAndTsDocResolver); const document = openDoc(filename); return { provider, document }; diff --git a/packages/language-server/test/plugins/typescript/features/ImplemenationProvider.test.ts b/packages/language-server/test/plugins/typescript/features/ImplemenationProvider.test.ts index 57ad86192..152542342 100644 --- a/packages/language-server/test/plugins/typescript/features/ImplemenationProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/ImplemenationProvider.test.ts @@ -29,7 +29,7 @@ describe('ImplementationProvider', function () { ); const lsAndTsDocResolver = new LSAndTSDocResolver( docManager, - [testDir], + [pathToUrl(testDir)], new LSConfigManager() ); const provider = new ImplementationProviderImpl(lsAndTsDocResolver); diff --git a/packages/language-server/test/plugins/typescript/features/TypeDefinitionProvider.test.ts b/packages/language-server/test/plugins/typescript/features/TypeDefinitionProvider.test.ts index f4e0baa44..e709bbd3c 100644 --- a/packages/language-server/test/plugins/typescript/features/TypeDefinitionProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/TypeDefinitionProvider.test.ts @@ -29,7 +29,7 @@ describe('TypeDefinitionProvider', function () { ); const lsAndTsDocResolver = new LSAndTSDocResolver( docManager, - [testDir], + [pathToUrl(testDir)], new LSConfigManager() ); const provider = new TypeDefinitionProviderImpl(lsAndTsDocResolver); 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 998896aa3..32c74ea97 100644 --- a/packages/language-server/test/plugins/typescript/features/UpdateImportsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/UpdateImportsProvider.test.ts @@ -20,7 +20,12 @@ const testDir = join(__dirname, '..'); const updateImportTestDir = join(testDir, 'testfiles', 'update-imports'); describe('UpdateImportsProviderImpl', function () { - serviceWarmup(this, updateImportTestDir, pathToUrl(testDir)); + serviceWarmup( + this, + updateImportTestDir, + pathToUrl(testDir), + join(testDir, 'testfiles', 'tsconfig.json') + ); async function setup(filename: string, useCaseSensitiveFileNames: boolean) { const docManager = new DocumentManager( @@ -33,7 +38,10 @@ describe('UpdateImportsProviderImpl', function () { new LSConfigManager(), { tsSystem: { ...ts.sys, useCaseSensitiveFileNames } } ); - const updateImportsProvider = new UpdateImportsProviderImpl(lsAndTsDocResolver); + const updateImportsProvider = new UpdateImportsProviderImpl( + lsAndTsDocResolver, + useCaseSensitiveFileNames + ); const filePath = join(updateImportTestDir, filename); const fileUri = pathToUrl(filePath); const document = docManager.openClientDocument({ diff --git a/packages/language-server/test/plugins/typescript/features/inlayHints/fixtures/tsconfig.json b/packages/language-server/test/plugins/typescript/features/inlayHints/fixtures/tsconfig.json new file mode 100644 index 000000000..f493a66ae --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/inlayHints/fixtures/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "allowJs": true, + "target": "ESNext", + "strict": true, + /** + This is actually not needed, but makes the tests faster + because TS does not look up other types. + */ + "types": ["svelte"] + } +} diff --git a/packages/language-server/test/plugins/typescript/test-utils.ts b/packages/language-server/test/plugins/typescript/test-utils.ts index c738bbd17..cf68336e8 100644 --- a/packages/language-server/test/plugins/typescript/test-utils.ts +++ b/packages/language-server/test/plugins/typescript/test-utils.ts @@ -8,6 +8,7 @@ import { LSConfigManager } from '../../../src/ls-config'; import { LSAndTSDocResolver } from '../../../src/plugins'; import { createGetCanonicalFileName, normalizePath, pathToUrl } from '../../../src/utils'; import { VERSION } from 'svelte/compiler'; +import { findTsConfigPath } from '../../../src/plugins/typescript/utils'; const isSvelte5Plus = Number(VERSION.split('.')[0]) >= 5; @@ -301,22 +302,22 @@ export function serviceWarmup( (textDocument) => new Document(textDocument.uri, textDocument.text) ); - const options = configFilePath ? { tsconfigPath: configFilePath } : undefined; const lsAndTsDocResolver = new LSAndTSDocResolver( docManager, [rootUri], - new LSConfigManager(), - options + new LSConfigManager() ); - const filePath = join(testDir, 'DoesNotMater.svelte'); - const document = docManager.openClientDocument({ - uri: pathToUrl(filePath), - text: ts.sys.readFile(filePath) || '' - }); + configFilePath ??= findTsConfigPath( + join(testDir, 'DoesNotMater.svelte'), + [rootUri], + ts.sys.fileExists, + createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames) + ); + + const ls = await lsAndTsDocResolver.getTSServiceByConfigPath(configFilePath); + ls.getService(); - const ls = await lsAndTsDocResolver.getTSService(filePath); - await lsAndTsDocResolver.getLSAndTSDoc(document); const projectReferences = ls.getResolvedProjectReferences(); if (projectReferences.length) { From 3fd24f887e92f5875a552194abf6180558ff66e6 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 16 Aug 2024 09:28:11 +0800 Subject: [PATCH 20/36] update import with existing services since it can run before or after project-files updates so either file might open a wrong service --- .../features/UpdateImportsProvider.ts | 76 ++++++++++++++++--- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts index aa46594ff..dc25048bc 100644 --- a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts @@ -6,14 +6,27 @@ import { WorkspaceEdit } from 'vscode-languageserver'; import { mapRangeToOriginal } from '../../../lib/documents'; -import { urlToPath } from '../../../utils'; +import { + createGetCanonicalFileName, + GetCanonicalFileName, + normalizePath, + urlToPath +} from '../../../utils'; import { FileRename, UpdateImportsProvider } from '../../interfaces'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; +import { forAllServices, LanguageServiceContainer } from '../service'; import { convertRange } from '../utils'; import { isKitTypePath, SnapshotMap } from './utils'; export class UpdateImportsProviderImpl implements UpdateImportsProvider { - constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} + constructor( + private readonly lsAndTsDocResolver: LSAndTSDocResolver, + useCaseSensitiveFileNames: boolean + ) { + this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + } + + private getCanonicalFileName: GetCanonicalFileName; async updateImports(fileRename: FileRename): Promise { // TODO does this handle folder moves/renames correctly? old/new path isn't a file then @@ -23,7 +36,47 @@ export class UpdateImportsProviderImpl implements UpdateImportsProvider { return null; } - const ls = await this.getLSForPath(newPath); + const services: LanguageServiceContainer[] = []; + await forAllServices((ls) => { + services.push(ls); + }); + + const documentChanges = new Map(); + for (const service of services) { + await this.updateImportForSingleService(oldPath, newPath, service, documentChanges); + } + + return { + documentChanges: Array.from(documentChanges.values()) + }; + } + + async updateImportForSingleService( + oldPath: string, + newPath: string, + lsContainer: LanguageServiceContainer, + documentChanges: Map + ) { + const ls = lsContainer.getService(); + const program = ls.getProgram(); + if (!program) { + return; + } + + const canonicalOldPath = this.getCanonicalFileName(normalizePath(oldPath)); + const canonicalNewPath = this.getCanonicalFileName(normalizePath(newPath)); + const hasFile = program.getSourceFiles().some((sf) => { + const normalizedFileName = this.getCanonicalFileName(normalizePath(sf.fileName)); + return ( + normalizedFileName.startsWith(canonicalOldPath) || + normalizedFileName.startsWith(canonicalNewPath) + ); + }); + + if (!hasFile) { + return; + } + const oldPathTsProgramCasing = ls.getProgram()?.getSourceFile(oldPath)?.fileName ?? oldPath; // `getEditsForFileRename` might take a while const fileChanges = ls @@ -75,12 +128,15 @@ export class UpdateImportsProviderImpl implements UpdateImportsProvider { return change; }); - const docs = new SnapshotMap(this.lsAndTsDocResolver); - const documentChanges = await Promise.all( + const docs = new SnapshotMap(this.lsAndTsDocResolver, lsContainer.tsconfigPath); + await Promise.all( updateImportsChanges.map(async (change) => { + if (documentChanges.has(change.fileName)) { + return; + } const snapshot = await docs.retrieve(change.fileName); - return TextDocumentEdit.create( + const edit = TextDocumentEdit.create( OptionalVersionedTextDocumentIdentifier.create(snapshot.getURL(), null), change.textChanges.map((edit) => { const range = mapRangeToOriginal( @@ -90,13 +146,9 @@ export class UpdateImportsProviderImpl implements UpdateImportsProvider { return TextEdit.replace(range, edit.newText); }) ); + + documentChanges.set(change.fileName, edit); }) ); - - return { documentChanges }; - } - - private async getLSForPath(path: string) { - return this.lsAndTsDocResolver.getLSForPath(path); } } From fc0da19deb9cbfd3a3536d8301278b21559f2ece Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 16 Aug 2024 12:50:32 +0800 Subject: [PATCH 21/36] fix snapshotMap in services without tsconfig --- .../src/plugins/typescript/LSAndTSDocResolver.ts | 16 +++++++++++----- .../src/plugins/typescript/TypeScriptPlugin.ts | 4 ++-- .../typescript/features/CallHierarchyProvider.ts | 6 +++--- .../typescript/features/CodeActionsProvider.ts | 10 +++++----- .../typescript/features/CompletionProvider.ts | 14 +++++++------- .../features/FindComponentReferencesProvider.ts | 2 +- .../features/FindFileReferencesProvider.ts | 2 +- .../features/FindReferencesProvider.ts | 4 ++-- .../features/ImplementationProvider.ts | 4 ++-- .../typescript/features/InlayHintProvider.ts | 4 ++-- .../typescript/features/RenameProvider.ts | 4 ++-- .../features/TypeDefinitionProvider.ts | 4 ++-- .../typescript/features/UpdateImportsProvider.ts | 2 +- .../src/plugins/typescript/features/utils.ts | 7 +------ .../src/plugins/typescript/service.ts | 2 +- .../features/UpdateImportsProvider.test.ts | 7 +------ .../test/plugins/typescript/test-utils.ts | 12 ++++++++++-- 17 files changed, 54 insertions(+), 50 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index a80d3cddb..b5452a6b7 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -155,7 +155,7 @@ export class LSAndTSDocResolver { tsDoc: SvelteDocumentSnapshot; lang: ts.LanguageService; userPreferences: ts.UserPreferences; - tsconfigPath: string; + lsContainer: LanguageServiceContainer; }> { const { tsDoc, lsContainer, userPreferences } = await this.getLSAndTSDocWorker(document); @@ -163,7 +163,7 @@ export class LSAndTSDocResolver { tsDoc, lang: lsContainer.getService(), userPreferences, - tsconfigPath: lsContainer.tsconfigPath + lsContainer }; } @@ -292,7 +292,10 @@ export class LSAndTSDocResolver { async getTSService(filePath?: string): Promise { if (this.options?.tsconfigPath) { - return this.getTSServiceByConfigPath(this.options.tsconfigPath); + return this.getTSServiceByConfigPath( + this.options.tsconfigPath, + dirname(this.options.tsconfigPath) + ); } if (!filePath) { throw new Error('Cannot call getTSService without filePath and without tsconfigPath'); @@ -300,8 +303,11 @@ export class LSAndTSDocResolver { return getService(filePath, this.workspaceUris, this.lsDocumentContext); } - async getTSServiceByConfigPath(tsconfigPath: string): Promise { - return getServiceForTsconfig(tsconfigPath, dirname(tsconfigPath), this.lsDocumentContext); + async getTSServiceByConfigPath( + tsconfigPath: string, + workspacePath: string + ): Promise { + return getServiceForTsconfig(tsconfigPath, workspacePath, this.lsDocumentContext); } private getUserPreferences(tsDoc: DocumentSnapshot): ts.UserPreferences { diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index 2b91ac323..264fe6665 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -386,7 +386,7 @@ export class TypeScriptPlugin } async getDefinitions(document: Document, position: Position): Promise { - const { lang, tsDoc, tsconfigPath } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { lang, tsDoc, lsContainer } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); const defs = lang.getDefinitionAndBoundSpan( tsDoc.filePath, @@ -397,7 +397,7 @@ export class TypeScriptPlugin return []; } - const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, lsContainer); snapshots.set(tsDoc.filePath, tsDoc); const result = await Promise.all( diff --git a/packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts b/packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts index 136906e27..3d60b40ac 100644 --- a/packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CallHierarchyProvider.ts @@ -41,7 +41,7 @@ export class CallHierarchyProviderImpl implements CallHierarchyProvider { position: Position, cancellationToken?: CancellationToken ): Promise { - const { lang, tsDoc, tsconfigPath } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { lang, tsDoc, lsContainer } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); if (cancellationToken?.isCancellationRequested) { return null; @@ -52,7 +52,7 @@ export class CallHierarchyProviderImpl implements CallHierarchyProvider { const itemsArray = Array.isArray(items) ? items : items ? [items] : []; - const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, lsContainer); snapshots.set(tsDoc.filePath, tsDoc); const program = lang.getProgram(); @@ -261,7 +261,7 @@ export class CallHierarchyProviderImpl implements CallHierarchyProvider { const program = lang.getProgram(); - const snapshots = new SnapshotMap(this.lsAndTsDocResolver, lsContainer.tsconfigPath); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, lsContainer); snapshots.set(tsDoc.filePath, tsDoc); const isComponentModulePosition = diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts index 8442bab74..1cec96eb2 100644 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts @@ -46,7 +46,6 @@ import { import { CompletionsProviderImpl } from './CompletionProvider'; import { findClosestContainingNode, - findContainingNode, FormatCodeBasis, getFormatCodeBasis, getNewScriptStartTag, @@ -56,6 +55,7 @@ import { } from './utils'; import { DiagnosticCode } from './DiagnosticsProvider'; import { createGetCanonicalFileName } from '../../../utils'; +import { LanguageServiceContainer } from '../service'; /** * TODO change this to protocol constant if it's part of the protocol @@ -156,7 +156,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { return codeAction; } - const { lang, tsDoc, userPreferences, tsconfigPath } = + const { lang, tsDoc, userPreferences, lsContainer } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); if (cancellationToken?.isCancellationRequested) { return codeAction; @@ -218,7 +218,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { await this.lsAndTsDocResolver.deleteSnapshot(virtualDocPath); } - const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, lsContainer); const fixActions: ts.CodeFixAction[] = [ { fixName: codeAction.data.fixName, @@ -553,7 +553,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { context: CodeActionContext, cancellationToken: CancellationToken | undefined ) { - const { lang, tsDoc, userPreferences, tsconfigPath } = await this.getLSAndTSDoc(document); + const { lang, tsDoc, userPreferences, lsContainer } = await this.getLSAndTSDoc(document); if (cancellationToken?.isCancellationRequested) { return []; @@ -613,7 +613,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { ); } - const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, lsContainer); snapshots.set(tsDoc.filePath, tsDoc); const codeActionsPromises = codeFixes.map(async (fix) => { diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index ffb044938..e7b5bec75 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -49,6 +49,7 @@ import { isPartOfImportStatement } from './utils'; import { isInTag as svelteIsInTag } from '../svelte-ast-utils'; +import { LanguageServiceContainer } from '../service'; export interface CompletionResolveInfo extends Pick, @@ -170,7 +171,7 @@ export class CompletionsProviderImpl implements CompletionsProvider 0 ? [] - : await this.getCustomElementCompletions(lang, document, tsDoc, position); + : this.getCustomElementCompletions(lang, lsContainer, document, tsDoc, position); const formatSettings = await this.configManager.getFormatCodeSettingsForFile( document, @@ -474,12 +475,13 @@ export class CompletionsProviderImpl implements CompletionsProvider { + ): CompletionItem[] | undefined { const offset = document.offsetAt(position); const tag = getNodeIfIsInHTMLStartTag(document.html, offset); @@ -499,9 +501,7 @@ export class CompletionsProviderImpl implements CompletionsProvider ref.definition.kind === ts.ScriptElementKind.alias)) { diff --git a/packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts b/packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts index 5cbf13b0b..693afaa49 100644 --- a/packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/ImplementationProvider.ts @@ -18,7 +18,7 @@ export class ImplementationProviderImpl implements ImplementationProvider { position: Position, cancellationToken?: CancellationToken ): Promise { - const { tsDoc, lang, tsconfigPath } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { tsDoc, lang, lsContainer } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); if (cancellationToken?.isCancellationRequested) { return null; @@ -27,7 +27,7 @@ export class ImplementationProviderImpl implements ImplementationProvider { const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); const implementations = lang.getImplementationAtPosition(tsDoc.filePath, offset); - const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, lsContainer); snapshots.set(tsDoc.filePath, tsDoc); if (!implementations) { diff --git a/packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts b/packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts index 7c48664e5..a11fb0f43 100644 --- a/packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/InlayHintProvider.ts @@ -41,7 +41,7 @@ export class InlayHintProviderImpl implements InlayHintProvider { return null; } - const { tsDoc, lang, tsconfigPath } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { tsDoc, lang, lsContainer } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); const inlayHints = lang.provideInlayHints( tsDoc.filePath, @@ -59,7 +59,7 @@ export class InlayHintProviderImpl implements InlayHintProvider { const renderFunctionReturnTypeLocation = renderFunction && this.getTypeAnnotationPosition(renderFunction); - const snapshotMap = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); + const snapshotMap = new SnapshotMap(this.lsAndTsDocResolver, lsContainer); snapshotMap.set(tsDoc.filePath, tsDoc); const convertPromises = inlayHints diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts index 3e67a18dc..3b9caf86e 100644 --- a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts @@ -65,7 +65,7 @@ export class RenameProviderImpl implements RenameProvider { position: Position, newName: string ): Promise { - const { lang, tsDoc, tsconfigPath } = await this.getLSAndTSDoc(document); + const { lang, tsDoc, lsContainer } = await this.getLSAndTSDoc(document); const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); @@ -85,7 +85,7 @@ export class RenameProviderImpl implements RenameProvider { return null; } - const docs = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); + const docs = new SnapshotMap(this.lsAndTsDocResolver, lsContainer); docs.set(tsDoc.filePath, tsDoc); let convertedRenameLocations: TsRenameLocation[] = await this.mapAndFilterRenameLocations( diff --git a/packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts b/packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts index 1cd619eaf..3c2ecf625 100644 --- a/packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/TypeDefinitionProvider.ts @@ -10,11 +10,11 @@ export class TypeDefinitionProviderImpl implements TypeDefinitionProvider { constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} async getTypeDefinition(document: Document, position: Position): Promise { - const { tsDoc, lang, tsconfigPath } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); + const { tsDoc, lang, lsContainer } = await this.lsAndTsDocResolver.getLSAndTSDoc(document); const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position)); const typeDefs = lang.getTypeDefinitionAtPosition(tsDoc.filePath, offset); - const snapshots = new SnapshotMap(this.lsAndTsDocResolver, tsconfigPath); + const snapshots = new SnapshotMap(this.lsAndTsDocResolver, lsContainer); snapshots.set(tsDoc.filePath, tsDoc); if (!typeDefs) { diff --git a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts index dc25048bc..54aa9a060 100644 --- a/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/UpdateImportsProvider.ts @@ -128,7 +128,7 @@ export class UpdateImportsProviderImpl implements UpdateImportsProvider { return change; }); - const docs = new SnapshotMap(this.lsAndTsDocResolver, lsContainer.tsconfigPath); + const docs = new SnapshotMap(this.lsAndTsDocResolver, lsContainer); await Promise.all( updateImportsChanges.map(async (change) => { if (documentChanges.has(change.fileName)) { diff --git a/packages/language-server/src/plugins/typescript/features/utils.ts b/packages/language-server/src/plugins/typescript/features/utils.ts index c43072f10..dba13177a 100644 --- a/packages/language-server/src/plugins/typescript/features/utils.ts +++ b/packages/language-server/src/plugins/typescript/features/utils.ts @@ -145,10 +145,9 @@ export function getStoreOffsetOf$storeDeclaration(text: string, $storeVarStart: export class SnapshotMap { private map = new FileMap(); - private sourceLs: LanguageServiceContainer | undefined; constructor( private resolver: LSAndTSDocResolver, - private readonly tsconfigPath: string + private sourceLs: LanguageServiceContainer ) {} set(fileName: string, snapshot: DocumentSnapshot) { @@ -165,10 +164,6 @@ export class SnapshotMap { return snapshot; } - if (!this.sourceLs) { - this.sourceLs = await this.resolver.getTSServiceByConfigPath(this.tsconfigPath); - } - const snap = this.sourceLs.snapshotManager.get(fileName) ?? // should not happen in most cases, diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 43b99742b..b1fe34463 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -456,7 +456,7 @@ async function createLanguageService( return new SnapshotManager( docContext.globalSnapshotsManager, parsedCommandLine.raw, - workspacePath, + configFileName ? dirname(configFileName) : workspacePath, tsSystem, parsedCommandLine.fileNames.map(normalizePath), parsedCommandLine.wildcardDirectories 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 32c74ea97..cdba08f4b 100644 --- a/packages/language-server/test/plugins/typescript/features/UpdateImportsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/UpdateImportsProvider.test.ts @@ -20,12 +20,7 @@ const testDir = join(__dirname, '..'); const updateImportTestDir = join(testDir, 'testfiles', 'update-imports'); describe('UpdateImportsProviderImpl', function () { - serviceWarmup( - this, - updateImportTestDir, - pathToUrl(testDir), - join(testDir, 'testfiles', 'tsconfig.json') - ); + serviceWarmup(this, updateImportTestDir, pathToUrl(testDir)); async function setup(filename: string, useCaseSensitiveFileNames: boolean) { const docManager = new DocumentManager( diff --git a/packages/language-server/test/plugins/typescript/test-utils.ts b/packages/language-server/test/plugins/typescript/test-utils.ts index cf68336e8..4e6972f57 100644 --- a/packages/language-server/test/plugins/typescript/test-utils.ts +++ b/packages/language-server/test/plugins/typescript/test-utils.ts @@ -6,7 +6,12 @@ import { DocumentManager, Document } from '../../../src/lib/documents'; import { FileMap } from '../../../src/lib/documents/fileCollection'; import { LSConfigManager } from '../../../src/ls-config'; import { LSAndTSDocResolver } from '../../../src/plugins'; -import { createGetCanonicalFileName, normalizePath, pathToUrl } from '../../../src/utils'; +import { + createGetCanonicalFileName, + normalizePath, + pathToUrl, + urlToPath +} from '../../../src/utils'; import { VERSION } from 'svelte/compiler'; import { findTsConfigPath } from '../../../src/plugins/typescript/utils'; @@ -315,7 +320,10 @@ export function serviceWarmup( createGetCanonicalFileName(ts.sys.useCaseSensitiveFileNames) ); - const ls = await lsAndTsDocResolver.getTSServiceByConfigPath(configFilePath); + const ls = await lsAndTsDocResolver.getTSServiceByConfigPath( + configFilePath, + configFilePath ? dirname(configFilePath) : urlToPath(rootUri)! + ); ls.getService(); const projectReferences = ls.getResolvedProjectReferences(); From 006e807687a2cb0e104ef8c59a5129e6a772ef30 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 16 Aug 2024 12:52:27 +0800 Subject: [PATCH 22/36] virtual doc won't pass the project files check so we have to directly access the lsContainer --- .../typescript/features/CodeActionsProvider.ts | 12 ++++++------ .../src/plugins/typescript/service.ts | 12 ++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts index 1cec96eb2..59def877c 100644 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts @@ -180,10 +180,11 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { const isImportFix = codeAction.data.fixName === FIX_IMPORT_FIX_NAME; const virtualDocInfo = isImportFix - ? await this.createVirtualDocumentForCombinedImportCodeFix( + ? this.createVirtualDocumentForCombinedImportCodeFix( document, getDiagnostics(), tsDoc, + lsContainer, lang ) : undefined; @@ -259,10 +260,11 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { * Do not use this in regular code action * This'll cause TypeScript to rebuild and invalidate caches every time. It'll be slow */ - private async createVirtualDocumentForCombinedImportCodeFix( + private createVirtualDocumentForCombinedImportCodeFix( document: Document, diagnostics: Diagnostic[], tsDoc: DocumentSnapshot, + lsContainer: LanguageServiceContainer, lang: ts.LanguageService ) { const virtualUri = document.uri + '.__virtual__.svelte'; @@ -314,10 +316,8 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { const virtualDoc = new Document(virtualUri, newText); virtualDoc.openedByClient = true; // let typescript know about the virtual document - // getLSAndTSDoc instead of getSnapshot so that project dirty state is correctly tracked by us - // otherwise, sometime the applied code fix might not be picked up by the language service - // because we think the project is still dirty and doesn't update the project version - await this.lsAndTsDocResolver.getLSAndTSDoc(virtualDoc); + lsContainer.openVirtualDocument(virtualDoc); + lsContainer.getService(); return { virtualDoc, diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index b1fe34463..cdeed8b2b 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -55,6 +55,7 @@ export interface LanguageServiceContainer { onPackageJsonChange(packageJsonPath: string): void; getTsConfigSvelteOptions(): { namespace: string }; getResolvedProjectReferences(): TsConfigInfo[]; + openVirtualDocument(document: Document): void; dispose(): void; } @@ -440,6 +441,7 @@ async function createLanguageService( onPackageJsonChange, getTsConfigSvelteOptions, getResolvedProjectReferences, + openVirtualDocument, dispose }; @@ -1137,6 +1139,16 @@ async function createLanguageService( .map((ref) => ensureTsConfigInfoUpToDate(normalizePath(ref.path))) .filter(isNotNullOrUndefined); } + + function openVirtualDocument(document: Document) { + const filePath = document.getFilePath(); + if (!filePath) { + return; + } + configFileForOpenFiles.set(filePath, tsconfigPath || workspacePath); + updateSnapshot(document); + scheduleUpdate(filePath); + } } /** From 6f8783305a608bbfa4ece4bca314aff96b18c31d Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 16 Aug 2024 12:53:08 +0800 Subject: [PATCH 23/36] breaking: put non-project files to default service --- .../src/plugins/typescript/service.ts | 12 ++++-- .../test/plugins/typescript/service.test.ts | 39 +++++++++++-------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index cdeed8b2b..5a378d246 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -156,7 +156,7 @@ export async function getService( ); }; - const tsconfigPath = + let tsconfigPath = configFileForOpenFiles.get(path) ?? findTsConfigPath(path, workspaceUris, fileExistsWithCache, getCanonicalFileName); @@ -171,9 +171,13 @@ export async function getService( return service; } - service = (await findDefaultServiceForFile(service, triedTsConfig)) ?? service; - configFileForOpenFiles.set(path, service.tsconfigPath); - return service; + const defaultService = await findDefaultServiceForFile(service, triedTsConfig); + if (defaultService) { + configFileForOpenFiles.set(path, defaultService.tsconfigPath); + return defaultService; + } + + tsconfigPath = ''; } // Find closer boundary: workspace uri or node_modules diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index ee195b079..a76c82773 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -54,6 +54,11 @@ describe('service', () => { }) ); + virtualSystem.writeFile( + path.join(dirPath, 'random.svelte'), + '' + ); + const ls = await getService( path.join(dirPath, 'random.svelte'), rootUris, @@ -89,6 +94,11 @@ describe('service', () => { }) ); + virtualSystem.writeFile( + path.join(dirPath, 'random.svelte'), + '' + ); + const ls = await getService( path.join(dirPath, 'random.svelte'), rootUris, @@ -160,6 +170,11 @@ describe('service', () => { }) ); + virtualSystem.writeFile( + path.join(dirPath, 'random.svelte'), + '' + ); + const { reloadPromise, docContextWithReload } = createReloadTester( { ...lsDocumentContext, watchTsConfig: true }, testAfterReload @@ -207,6 +222,11 @@ describe('service', () => { }) ); + virtualSystem.writeFile( + path.join(dirPath, 'random.svelte'), + '' + ); + const { reloadPromise, docContextWithReload } = createReloadTester( { ...lsDocumentContext, watchTsConfig: true }, testAfterReload @@ -304,18 +324,9 @@ describe('service', () => { it('can open client file that do not exist in fs', async () => { const dirPath = getRandomVirtualDirPath(testDir); - const { virtualSystem, lsDocumentContext, rootUris } = setup(); - - virtualSystem.writeFile( - path.join(dirPath, 'tsconfig.json'), - JSON.stringify({ - compilerOptions: { - checkJs: true, - strict: true - } - }) - ); + const { lsDocumentContext, rootUris } = setup(); + // don't need tsconfig because files doesn't exist in fs goes to a service with default config const ls = await getService( path.join(dirPath, 'random.svelte'), rootUris, @@ -380,11 +391,7 @@ describe('service', () => { return fileExists(realPath); }; - const ls = await getService( - path.join(package1, 'DoNotMatter.svelte'), - rootUris, - lsDocumentContext - ); + const ls = await getService(importing, rootUris, lsDocumentContext); assert.deepStrictEqual(getSemanticDiagnosticsMessages(ls, importing), []); }); From cd76dbe7f73e0321cf951031b4da21f4625a1e97 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 16 Aug 2024 16:05:52 +0800 Subject: [PATCH 24/36] skip type-check for shim files --- .../src/plugins/typescript/service.ts | 48 +++++++++++++------ packages/language-server/src/svelte-check.ts | 1 + packages/svelte-check/test/tsconfig.json | 2 +- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 5a378d246..613cf04ab 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -56,6 +56,7 @@ export interface LanguageServiceContainer { getTsConfigSvelteOptions(): { namespace: string }; getResolvedProjectReferences(): TsConfigInfo[]; openVirtualDocument(document: Document): void; + isShimFiles(filePath: string): boolean; dispose(): void; } @@ -353,22 +354,8 @@ async function createLanguageService( ? importSvelte(tsconfigPath || workspacePath) : undefined; - const isSvelte3 = sveltePackageInfo.version.major === 3; - const svelteHtmlDeclaration = isSvelte3 - ? undefined - : join(sveltePackageInfo.path, 'svelte-html.d.ts'); - const svelteHtmlFallbackIfNotExist = - svelteHtmlDeclaration && tsSystem.fileExists(svelteHtmlDeclaration) - ? svelteHtmlDeclaration - : './svelte-jsx-v4.d.ts'; - const changedFilesForExportCache = new Set(); - - const svelteTsxFiles = ( - isSvelte3 - ? ['./svelte-shims.d.ts', './svelte-jsx.d.ts', './svelte-native-jsx.d.ts'] - : ['./svelte-shims-v4.d.ts', svelteHtmlFallbackIfNotExist, './svelte-native-jsx.d.ts'] - ).map((f) => tsSystem.resolvePath(resolve(svelteTsPath, f))); + const svelteTsxFiles = getSvelteShimFiles(); let languageServiceReducedMode = false; let projectVersion = 0; @@ -446,6 +433,7 @@ async function createLanguageService( getTsConfigSvelteOptions, getResolvedProjectReferences, openVirtualDocument, + isShimFiles, dispose }; @@ -1153,6 +1141,36 @@ async function createLanguageService( updateSnapshot(document); scheduleUpdate(filePath); } + + function getSvelteShimFiles() { + const isSvelte3 = sveltePackageInfo.version.major === 3; + const svelteHtmlDeclaration = isSvelte3 + ? undefined + : join(sveltePackageInfo.path, 'svelte-html.d.ts'); + const svelteHtmlFallbackIfNotExist = + svelteHtmlDeclaration && tsSystem.fileExists(svelteHtmlDeclaration) + ? svelteHtmlDeclaration + : './svelte-jsx-v4.d.ts'; + + const svelteTsxFiles = ( + isSvelte3 + ? ['./svelte-shims.d.ts', './svelte-jsx.d.ts', './svelte-native-jsx.d.ts'] + : [ + './svelte-shims-v4.d.ts', + svelteHtmlFallbackIfNotExist, + './svelte-native-jsx.d.ts' + ] + ).map((f) => tsSystem.resolvePath(resolve(svelteTsPath, f))); + + const result = new FileSet(tsSystem.useCaseSensitiveFileNames); + + svelteTsxFiles.forEach((f) => result.add(normalizePath(f))); + return result; + } + + function isShimFiles(filePath: string) { + return svelteTsxFiles.has(normalizePath(filePath)); + } } /** diff --git a/packages/language-server/src/svelte-check.ts b/packages/language-server/src/svelte-check.ts index b6100e60c..57ab91586 100644 --- a/packages/language-server/src/svelte-check.ts +++ b/packages/language-server/src/svelte-check.ts @@ -211,6 +211,7 @@ export class SvelteCheck { const skipDiagnosticsForFile = (options.skipLibCheck && file.isDeclarationFile) || (options.skipDefaultLibCheck && file.hasNoDefaultLib) || + lsContainer.isShimFiles(file.fileName) || // ignore JS files in node_modules /\/node_modules\/.+\.(c|m)?js$/.test(file.fileName); const snapshot = lsContainer.snapshotManager.get(file.fileName) as diff --git a/packages/svelte-check/test/tsconfig.json b/packages/svelte-check/test/tsconfig.json index 15458e89c..d1df1fdec 100644 --- a/packages/svelte-check/test/tsconfig.json +++ b/packages/svelte-check/test/tsconfig.json @@ -1,6 +1,6 @@ { - "extends": "@tsconfig/node12/tsconfig.json", "compilerOptions": { + "target": "ES2019", "moduleResolution": "node", "strict": true, "allowJs": true, From 1395cfb16dc70fcdb752b2faf86d48f448b9de61 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 16 Aug 2024 16:10:46 +0800 Subject: [PATCH 25/36] change test config target --- packages/svelte-check/test/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte-check/test/tsconfig.json b/packages/svelte-check/test/tsconfig.json index d1df1fdec..86649d013 100644 --- a/packages/svelte-check/test/tsconfig.json +++ b/packages/svelte-check/test/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2019", + "target": "ESNext", "moduleResolution": "node", "strict": true, "allowJs": true, From ea8c0bdead44d6f2f30b78a12de20dbb8ede2c0e Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 19 Aug 2024 09:03:31 -0700 Subject: [PATCH 26/36] chore: switch from fast-glob to fdir (#2433) closes #2397 fixes #2364 --------- Co-authored-by: Simon Holthausen --- packages/language-server/package.json | 2 +- .../src/lib/documents/configLoader.ts | 27 +++++++---- .../test/lib/documents/configLoader.test.ts | 44 ++++++++++++----- packages/svelte-check/package.json | 2 +- packages/svelte-check/src/index.ts | 47 +++++++++++++++++-- packages/svelte-check/src/options.ts | 6 +-- pnpm-lock.yaml | 22 ++++++--- 7 files changed, 111 insertions(+), 39 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 680e21419..f79ba9ede 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -50,7 +50,7 @@ "@vscode/emmet-helper": "2.8.4", "chokidar": "^3.4.1", "estree-walker": "^2.0.1", - "fast-glob": "^3.2.7", + "fdir": "^6.2.0", "lodash": "^4.17.21", "prettier": "~3.2.5", "prettier-plugin-svelte": "^3.2.2", diff --git a/packages/language-server/src/lib/documents/configLoader.ts b/packages/language-server/src/lib/documents/configLoader.ts index 4d7e583c6..c0fef4156 100644 --- a/packages/language-server/src/lib/documents/configLoader.ts +++ b/packages/language-server/src/lib/documents/configLoader.ts @@ -4,7 +4,7 @@ import { CompileOptions } from 'svelte/types/compiler/interfaces'; // @ts-ignore import { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; import { importSveltePreprocess } from '../../importPackage'; -import _glob from 'fast-glob'; +import { fdir } from 'fdir'; import _path from 'path'; import _fs from 'fs'; import { pathToFileURL, URL } from 'url'; @@ -47,6 +47,8 @@ const _dynamicImport = new Function('modulePath', 'return import(modulePath)') a modulePath: URL ) => Promise; +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. @@ -61,7 +63,7 @@ export class ConfigLoader { private disabled = false; constructor( - private globSync: typeof _glob.sync, + private globSync: typeof fdir, private fs: Pick, private path: Pick, private dynamicImport: typeof _dynamicImport @@ -84,12 +86,19 @@ export class ConfigLoader { Logger.log('Trying to load configs for', directory); try { - const pathResults = this.globSync('**/svelte.config.{js,cjs,mjs}', { - cwd: directory, - // the second pattern is necessary because else fast-glob treats .tmp/../node_modules/.. as a valid match for some reason - ignore: ['**/node_modules/**', '**/.*/**'], - onlyFiles: true - }); + 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('/.'); + }) + .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) { @@ -296,4 +305,4 @@ export class ConfigLoader { } } -export const configLoader = new ConfigLoader(_glob.sync, _fs, _path, _dynamicImport); +export const configLoader = new ConfigLoader(fdir, _fs, _path, _dynamicImport); diff --git a/packages/language-server/test/lib/documents/configLoader.test.ts b/packages/language-server/test/lib/documents/configLoader.test.ts index a3d3d708e..066e23aa9 100644 --- a/packages/language-server/test/lib/documents/configLoader.test.ts +++ b/packages/language-server/test/lib/documents/configLoader.test.ts @@ -19,6 +19,29 @@ describe('ConfigLoader', () => { return path.join(...filePath.split('/')); } + function mockFdir(results: string[] | (() => string[])): any { + return class { + withPathSeparator() { + return this; + } + exclude() { + return this; + } + filter() { + return this; + } + withRelativePaths() { + return this; + } + crawl() { + return this; + } + sync() { + return typeof results === 'function' ? results() : results; + } + }; + } + async function assertFindsConfig( configLoader: ConfigLoader, filePath: string, @@ -32,7 +55,7 @@ describe('ConfigLoader', () => { 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']) as any, + mockFdir(['svelte.config.js', 'below/svelte.config.js']), { existsSync: () => true }, path, (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } }) @@ -63,7 +86,7 @@ describe('ConfigLoader', () => { it('finds first above if none found inside/below directory', async () => { const configLoader = new ConfigLoader( - () => [], + mockFdir([]), { existsSync: (p) => typeof p === 'string' && p.endsWith(path.join('some', 'svelte.config.js')) @@ -78,7 +101,7 @@ describe('ConfigLoader', () => { it('adds fallback if no config found', async () => { const configLoader = new ConfigLoader( - () => [], + mockFdir([]), { existsSync: () => false }, path, (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } }) @@ -99,14 +122,14 @@ describe('ConfigLoader', () => { let firstGlobCall = true; let nrImportCalls = 0; const configLoader = new ConfigLoader( - (() => { + mockFdir(() => { if (firstGlobCall) { firstGlobCall = false; return ['svelte.config.js']; } else { return []; } - }) as any, + }), { existsSync: (p) => typeof p === 'string' && @@ -140,11 +163,8 @@ describe('ConfigLoader', () => { }); it('can deal with missing config', () => { - const configLoader = new ConfigLoader( - () => [], - { existsSync: () => false }, - path, - () => Promise.resolve('unimportant') + const configLoader = new ConfigLoader(mockFdir([]), { existsSync: () => false }, path, () => + Promise.resolve('unimportant') ); assert.deepStrictEqual( configLoader.getConfig(normalizePath('/some/file.svelte')), @@ -154,7 +174,7 @@ describe('ConfigLoader', () => { it('should await config', async () => { const configLoader = new ConfigLoader( - () => [], + mockFdir([]), { existsSync: () => true }, path, (module: URL) => Promise.resolve({ default: { preprocess: module.toString() } }) @@ -168,7 +188,7 @@ describe('ConfigLoader', () => { it('should not load config when disabled', async () => { const moduleLoader = spy(); const configLoader = new ConfigLoader( - () => [], + mockFdir([]), { existsSync: () => true }, path, moduleLoader diff --git a/packages/svelte-check/package.json b/packages/svelte-check/package.json index 9781ce03b..1bb3c4550 100644 --- a/packages/svelte-check/package.json +++ b/packages/svelte-check/package.json @@ -28,6 +28,7 @@ "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", "chokidar": "^3.4.1", + "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4", "svelte-preprocess": "^5.1.3" @@ -49,7 +50,6 @@ "@rollup/plugin-typescript": "^10.0.0", "@types/sade": "^1.7.2", "builtin-modules": "^3.3.0", - "fast-glob": "^3.2.7", "rollup": "3.7.5", "rollup-plugin-cleanup": "^3.2.0", "rollup-plugin-copy": "^3.4.0", diff --git a/packages/svelte-check/src/index.ts b/packages/svelte-check/src/index.ts index 6e64414ed..3fc99a69d 100644 --- a/packages/svelte-check/src/index.ts +++ b/packages/svelte-check/src/index.ts @@ -4,7 +4,7 @@ import { watch } from 'chokidar'; import * as fs from 'fs'; -import glob from 'fast-glob'; +import { fdir } from 'fdir'; import * as path from 'path'; import { SvelteCheck, SvelteCheckOptions } from 'svelte-language-server'; import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-protocol'; @@ -30,11 +30,48 @@ async function openAllDocuments( filePathsToIgnore: string[], svelteCheck: SvelteCheck ) { - const files = await glob('**/*.svelte', { - cwd: workspaceUri.fsPath, - ignore: ['node_modules/**'].concat(filePathsToIgnore.map((ignore) => `${ignore}/**`)) + const offset = workspaceUri.fsPath.length + 1; + // We support a very limited subset of glob patterns: You can only have ** at the end or the start + const ignored: Array<(path: string) => boolean> = filePathsToIgnore.map((i) => { + if (i.endsWith('**')) i = i.slice(0, -2); + + if (i.startsWith('**')) { + i = i.slice(2); + + if (i.includes('*')) + throw new Error( + 'Invalid svelte-check --ignore pattern: Only ** at the start or end is supported' + ); + + return (path) => path.includes(i); + } + + if (i.includes('*')) + throw new Error( + 'Invalid svelte-check --ignore pattern: Only ** at the start or end is supported' + ); + + return (path) => path.startsWith(i); }); - const absFilePaths = files.map((f) => path.resolve(workspaceUri.fsPath, f)); + const isIgnored = (path: string) => { + path = path.slice(offset); + for (const i of ignored) { + if (i(path)) { + return true; + } + } + return false; + }; + const absFilePaths = await new fdir() + .filter((path) => path.endsWith('.svelte') && !isIgnored(path)) + .exclude((_, path) => { + path = path.slice(offset); + return path.startsWith('.') || path.startsWith('node_modules'); + }) + .withPathSeparator('/') + .withFullPaths() + .crawl(workspaceUri.fsPath) + .withPromise(); for (const absFilePath of absFilePaths) { const text = fs.readFileSync(absFilePath, 'utf-8'); diff --git a/packages/svelte-check/src/options.ts b/packages/svelte-check/src/options.ts index bf270e3c4..aa5e8e6c1 100644 --- a/packages/svelte-check/src/options.ts +++ b/packages/svelte-check/src/options.ts @@ -73,7 +73,7 @@ export function parseOptions(cb: (opts: SvelteCheckCliOptions) => any) { watch: !!opts.watch, preserveWatchOutput: !!opts.preserveWatchOutput, tsconfig: getTsconfig(opts, workspaceUri.fsPath), - filePathsToIgnore: getFilepathsToIgnore(opts), + filePathsToIgnore: opts.ignore?.split(',') || [], failOnWarnings: !!opts['fail-on-warnings'], compilerWarnings: getCompilerWarnings(opts), diagnosticSources: getDiagnosticSources(opts), @@ -180,10 +180,6 @@ function getDiagnosticSources(opts: Record): DiagnosticSource[] { : diagnosticSources; } -function getFilepathsToIgnore(opts: Record): string[] { - return opts.ignore?.split(',') || []; -} - const thresholds = ['warning', 'error'] as const; type Threshold = (typeof thresholds)[number]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cad3a3cb4..cdf7052eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,9 +36,9 @@ importers: estree-walker: specifier: ^2.0.1 version: 2.0.2 - fast-glob: - specifier: ^3.2.7 - version: 3.2.12 + fdir: + specifier: ^6.2.0 + version: 6.2.0 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -118,6 +118,9 @@ importers: chokidar: specifier: ^3.4.1 version: 3.5.3 + fdir: + specifier: ^6.2.0 + version: 6.2.0 picocolors: specifier: ^1.0.0 version: 1.0.0 @@ -149,9 +152,6 @@ importers: builtin-modules: specifier: ^3.3.0 version: 3.3.0 - fast-glob: - specifier: ^3.2.7 - version: 3.2.12 rollup: specifier: 3.7.5 version: 3.7.5 @@ -742,6 +742,14 @@ packages: fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + fdir@6.2.0: + resolution: {integrity: sha512-9XaWcDl0riOX5j2kYfy0kKdg7skw3IY6kA4LFT8Tk2yF9UdrADUy8D6AJuBLtf7ISm/MksumwAHE3WVbMRyCLw==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -1801,6 +1809,8 @@ snapshots: dependencies: reusify: 1.0.4 + fdir@6.2.0: {} + fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 From 7a1f80ae7e08e64a3ad08dc0c59d3ae1c99f34c2 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 19 Aug 2024 18:11:45 +0200 Subject: [PATCH 27/36] chore: remove svelte-preprocess dependency from svelte-check Since language-server doesn't have a dependency on it anymore, we don't need it for svelte-check anymore, too --- packages/svelte-check/package.json | 3 +- packages/svelte-check/rollup.config.mjs | 1 - pnpm-lock.yaml | 136 ------------------------ 3 files changed, 1 insertion(+), 139 deletions(-) diff --git a/packages/svelte-check/package.json b/packages/svelte-check/package.json index 1bb3c4550..f253baf55 100644 --- a/packages/svelte-check/package.json +++ b/packages/svelte-check/package.json @@ -30,8 +30,7 @@ "chokidar": "^3.4.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", - "sade": "^1.7.4", - "svelte-preprocess": "^5.1.3" + "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", diff --git a/packages/svelte-check/rollup.config.mjs b/packages/svelte-check/rollup.config.mjs index 2488f2bae..e5346edee 100644 --- a/packages/svelte-check/rollup.config.mjs +++ b/packages/svelte-check/rollup.config.mjs @@ -65,7 +65,6 @@ export default [ 'sade', 'svelte', 'svelte/compiler', - 'svelte-preprocess', '@jridgewell/trace-mapping' // import-fresh removed some time ago, no dependency uses it anymore. // if it creeps back in check if the dependency uses a version that diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdf7052eb..2ebe4e95d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,9 +127,6 @@ importers: sade: specifier: ^1.7.4 version: 1.8.1 - svelte-preprocess: - specifier: ^5.1.3 - version: 5.1.3(svelte@3.57.0)(typescript@5.5.2) devDependencies: '@rollup/plugin-commonjs': specifier: ^24.0.0 @@ -489,9 +486,6 @@ packages: '@types/prettier@2.7.2': resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==} - '@types/pug@2.0.6': - resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} - '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -589,9 +583,6 @@ packages: browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -683,10 +674,6 @@ packages: resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} engines: {node: '>=8'} - detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -709,9 +696,6 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - es6-promise@3.3.1: - resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} - escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -954,10 +938,6 @@ packages: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} - magic-string@0.30.7: - resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} - engines: {node: '>=12'} - make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -969,10 +949,6 @@ packages: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} - min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -984,13 +960,6 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - mocha@9.2.2: resolution: {integrity: sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==} engines: {node: '>= 12.0.0'} @@ -1108,10 +1077,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - hasBin: true - rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -1148,9 +1113,6 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - sander@0.5.1: - resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} - semver@7.5.1: resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} engines: {node: '>=10'} @@ -1178,10 +1140,6 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - sorcery@0.11.0: - resolution: {integrity: sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==} - hasBin: true - source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -1204,10 +1162,6 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1228,43 +1182,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-preprocess@5.1.3: - resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==} - engines: {node: '>= 16.0.0', pnpm: ^8.0.0} - peerDependencies: - '@babel/core': ^7.10.2 - coffeescript: ^2.5.1 - less: ^3.11.3 || ^4.0.0 - postcss: ^7 || ^8 - postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 - pug: ^3.0.0 - sass: ^1.26.8 - stylus: ^0.55.0 - sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0 - svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 - typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0' - peerDependenciesMeta: - '@babel/core': - optional: true - coffeescript: - optional: true - less: - optional: true - postcss: - optional: true - postcss-load-config: - optional: true - pug: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - typescript: - optional: true - svelte@3.57.0: resolution: {integrity: sha512-WMXEvF+RtAaclw0t3bPDTUe19pplMlfyKDsixbHQYgCWi9+O9VN0kXU1OppzrB9gPAvz4NALuoca2LfW2bOjTQ==} engines: {node: '>= 8'} @@ -1578,8 +1495,6 @@ snapshots: '@types/prettier@2.7.2': {} - '@types/pug@2.0.6': {} - '@types/resolve@1.20.2': {} '@types/sade@1.7.4': @@ -1670,8 +1585,6 @@ snapshots: browser-stdout@1.3.1: {} - buffer-crc32@0.2.13: {} - buffer-from@1.1.2: {} builtin-modules@3.3.0: {} @@ -1764,8 +1677,6 @@ snapshots: rimraf: 3.0.2 slash: 3.0.0 - detect-indent@6.1.0: {} - diff@4.0.2: {} diff@5.0.0: {} @@ -1783,8 +1694,6 @@ snapshots: emoji-regex@8.0.0: {} - es6-promise@3.3.1: {} - escalade@3.1.1: {} escape-string-regexp@1.0.5: {} @@ -2012,10 +1921,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - magic-string@0.30.7: - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - make-error@1.3.6: {} merge2@1.4.1: {} @@ -2025,8 +1930,6 @@ snapshots: braces: 3.0.2 picomatch: 2.3.1 - min-indent@1.0.1: {} - minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -2039,12 +1942,6 @@ snapshots: dependencies: brace-expansion: 2.0.1 - minimist@1.2.8: {} - - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - mocha@9.2.2: dependencies: '@ungap/promise-all-settled': 1.1.2 @@ -2168,10 +2065,6 @@ snapshots: reusify@1.0.4: {} - rimraf@2.7.1: - dependencies: - glob: 7.2.3 - rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -2212,13 +2105,6 @@ snapshots: safe-buffer@5.2.1: {} - sander@0.5.1: - dependencies: - es6-promise: 3.3.1 - graceful-fs: 4.2.11 - mkdirp: 0.5.6 - rimraf: 2.7.1 - semver@7.5.1: dependencies: lru-cache: 6.0.0 @@ -2246,13 +2132,6 @@ snapshots: slash@3.0.0: {} - sorcery@0.11.0: - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - buffer-crc32: 0.2.13 - minimist: 1.2.8 - sander: 0.5.1 - source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -2274,10 +2153,6 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-indent@3.0.0: - dependencies: - min-indent: 1.0.1 - strip-json-comments@3.1.1: {} supports-color@5.5.0: @@ -2294,17 +2169,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-preprocess@5.1.3(svelte@3.57.0)(typescript@5.5.2): - dependencies: - '@types/pug': 2.0.6 - detect-indent: 6.1.0 - magic-string: 0.30.7 - sorcery: 0.11.0 - strip-indent: 3.0.0 - svelte: 3.57.0 - optionalDependencies: - typescript: 5.5.2 - svelte@3.57.0: {} tiny-glob@0.2.9: From 6dac14e1a073a44286e69e5a5c1a28f2cfa15ea3 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Tue, 20 Aug 2024 13:07:07 +0800 Subject: [PATCH 28/36] simplify default compiler options logic --- .../src/plugins/typescript/service.ts | 144 ++++++------------ .../test/plugins/typescript/service.test.ts | 25 +++ 2 files changed, 72 insertions(+), 97 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 613cf04ab..8e3684c0c 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -672,7 +672,7 @@ async function createLanguageService( function getParsedConfig() { let compilerOptions: ts.CompilerOptions; let parsedConfig: ts.ParsedCommandLine; - let extendedConfigPaths: Set; + let extendedConfigPaths: Set | undefined; if (tsconfigPath) { const info = ensureTsConfigInfoUpToDate(tsconfigPath); @@ -683,12 +683,11 @@ async function createLanguageService( } compilerOptions = info.parsedCommandLine.options; parsedConfig = info.parsedCommandLine; - extendedConfigPaths = info.extendedConfigPaths ?? new Set(); + extendedConfigPaths = info.extendedConfigPaths; } else { const config = parseDefaultCompilerOptions(); compilerOptions = config.compilerOptions; parsedConfig = config.parsedConfig; - extendedConfigPaths = config.extendedConfigPaths; } if ( @@ -739,94 +738,12 @@ async function createLanguageService( } function parseDefaultCompilerOptions() { - const forcedCompilerOptions: ts.CompilerOptions = { - allowNonTsExtensions: true, - target: ts.ScriptTarget.Latest, - allowJs: true, - noEmit: true, - declaration: false, - skipLibCheck: true - }; - - // always let ts parse config to get default compilerOption - let configJson = - (tsconfigPath && ts.readConfigFile(tsconfigPath, tsSystem.readFile).config) || - getDefaultJsConfig(); - - // Only default exclude when no extends for now - if (!configJson.extends) { - configJson = Object.assign( - { - exclude: getDefaultExclude() - }, - configJson - ); - } - - const { cacheMonitorProxy, extendedConfigPaths } = monitorExtendedConfig(); - - const parsedConfig = ts.parseJsonConfigFileContent( - configJson, - tsSystem, - workspacePath, - forcedCompilerOptions, - tsconfigPath, - undefined, - getExtraExtensions(), - cacheMonitorProxy - ); - - const compilerOptions: ts.CompilerOptions = { - ...parsedConfig.options, - ...forcedCompilerOptions - }; - - return { compilerOptions, parsedConfig, extendedConfigPaths }; - } - - function monitorExtendedConfig() { - const extendedConfigPaths = new Set(); - const { extendedConfigCache } = docContext; - const cacheMonitorProxy = { - ...docContext.extendedConfigCache, - get(key: string) { - extendedConfigPaths.add(key); - return extendedConfigCache.get(key); - }, - has(key: string) { - extendedConfigPaths.add(key); - return extendedConfigCache.has(key); - }, - set(key: string, value: ts.ExtendedConfigCacheEntry) { - extendedConfigPaths.add(key); - return extendedConfigCache.set(key, value); - } - }; - return { cacheMonitorProxy, extendedConfigPaths }; - } - - function getExtraExtensions(): readonly ts.FileExtensionInfo[] | undefined { - return [ - { - extension: 'svelte', - isMixedContent: true, - // Deferred was added in a later TS version, fall back to tsx - // If Deferred exists, this means that all Svelte files are included - // in parsedConfig.fileNames - scriptKind: ts.ScriptKind.Deferred ?? ts.ScriptKind.TS - } - ]; - } - - /** - * This should only be used when there's no jsconfig/tsconfig at all - */ - function getDefaultJsConfig(): { - compilerOptions: ts.CompilerOptions; - include: string[]; - } { - return { + let configJson = { compilerOptions: { + allowJs: true, + noEmit: true, + declaration: false, + skipLibCheck: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true }, @@ -834,10 +751,17 @@ async function createLanguageService( // with potentially completely unrelated .ts/.js files: include: [] }; - } - function getDefaultExclude() { - return ['node_modules', ...ignoredBuildDirectories]; + const parsedConfig = ts.parseJsonConfigFileContent(configJson, tsSystem, workspacePath); + + const compilerOptions: ts.CompilerOptions = { + ...parsedConfig.options, + target: ts.ScriptTarget.Latest, + allowNonTsExtensions: true, + moduleResolution: ts.ModuleResolutionKind.Node10 + }; + + return { compilerOptions, parsedConfig }; } /** @@ -874,10 +798,10 @@ async function createLanguageService( } function watchConfigFiles( - extendedConfigPaths: Set, + extendedConfigPaths: Set | undefined, parsedCommandLine: ts.ParsedCommandLine ) { - const tsconfigDependencies = Array.from(extendedConfigPaths).concat( + const tsconfigDependencies = Array.from(extendedConfigPaths ?? []).concat( parsedCommandLine.projectReferences?.map((r) => r.path) ?? [] ); tsconfigDependencies.forEach((configPath) => { @@ -1074,7 +998,24 @@ async function createLanguageService( const json = ts.parseJsonText(configFilePath, content); - const { cacheMonitorProxy, extendedConfigPaths } = monitorExtendedConfig(); + const extendedConfigPaths = new Set(); + const { extendedConfigCache } = docContext; + const cacheMonitorProxy = { + ...docContext.extendedConfigCache, + get(key: string) { + extendedConfigPaths.add(key); + return extendedConfigCache.get(key); + }, + has(key: string) { + extendedConfigPaths.add(key); + return extendedConfigCache.has(key); + }, + set(key: string, value: ts.ExtendedConfigCacheEntry) { + extendedConfigPaths.add(key); + return extendedConfigCache.set(key, value); + } + }; + // TypeScript will throw if the parsedCommandLine doesn't include the sourceFile for the config file // i.e. it must be directly parse from the json text instead of a javascript object like we do in getParsedConfig const parsedCommandLine = ts.parseJsonSourceFileConfigFileContent( @@ -1084,7 +1025,16 @@ async function createLanguageService( /*existingOptions*/ undefined, configFilePath, /*resolutionStack*/ undefined, - getExtraExtensions(), + [ + { + extension: 'svelte', + isMixedContent: true, + // Deferred was added in a later TS version, fall back to tsx + // If Deferred exists, this means that all Svelte files are included + // in parsedConfig.fileNames + scriptKind: ts.ScriptKind.Deferred ?? ts.ScriptKind.TS + } + ], cacheMonitorProxy ); diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index a76c82773..c16517557 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -77,6 +77,31 @@ describe('service', () => { }); }); + it('can loads default tsconfig', async () => { + const dirPath = getRandomVirtualDirPath(testDir); + const { lsDocumentContext, rootUris } = setup(); + + const ls = await getService( + path.join(dirPath, 'random.svelte'), + rootUris, + lsDocumentContext + ); + + assert.deepStrictEqual(ls.compilerOptions, { + allowJs: true, + allowSyntheticDefaultImports: true, + allowNonTsExtensions: true, + configFilePath: undefined, + declaration: false, + maxNodeModuleJsDepth: 2, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Node10, + noEmit: true, + skipLibCheck: true, + target: ts.ScriptTarget.ESNext + }); + }); + it('patch release document so dispose do not throw', async () => { // testing this because the patch rely on ts implementation details // and we want to be aware of the changes From f580633e2a0f8c9fb742550f1568b56fb164faf0 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 20 Aug 2024 11:26:42 +0200 Subject: [PATCH 29/36] breaking: throw when `--ignore` is used without `--no-tsconfig` #1074 --- packages/svelte-check/src/options.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/svelte-check/src/options.ts b/packages/svelte-check/src/options.ts index aa5e8e6c1..b43dd7695 100644 --- a/packages/svelte-check/src/options.ts +++ b/packages/svelte-check/src/options.ts @@ -67,12 +67,18 @@ export function parseOptions(cb: (opts: SvelteCheckCliOptions) => any) { ) .action((opts) => { const workspaceUri = getWorkspaceUri(opts); + const tsconfig = getTsconfig(opts, workspaceUri.fsPath); + + if (opts.ignore && tsconfig) { + throwError('`--ignore` only has an effect when using `--no-tsconfig`'); + } + cb({ workspaceUri, outputFormat: getOutputFormat(opts), watch: !!opts.watch, preserveWatchOutput: !!opts.preserveWatchOutput, - tsconfig: getTsconfig(opts, workspaceUri.fsPath), + tsconfig, filePathsToIgnore: opts.ignore?.split(',') || [], failOnWarnings: !!opts['fail-on-warnings'], compilerWarnings: getCompilerWarnings(opts), @@ -141,11 +147,15 @@ function getTsconfig(myArgs: Record, workspacePath: string) { tsconfig = path.join(workspacePath, tsconfig); } if (tsconfig && !fs.existsSync(tsconfig)) { - throw new Error('Could not find tsconfig/jsconfig file at ' + myArgs.tsconfig); + throwError('Could not find tsconfig/jsconfig file at ' + myArgs.tsconfig); } return tsconfig; } +function throwError(msg: string) { + throw new Error('Invalid svelte-check CLI args: ' + msg); +} + function getCompilerWarnings(opts: Record) { return stringToObj(opts['compiler-warnings']); From e9a22d0b5c6182810574e9019f68ee4646d1c245 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:06:05 +0200 Subject: [PATCH 30/36] set target --- packages/language-server/src/plugins/typescript/service.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 8e3684c0c..1762d1180 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -698,6 +698,7 @@ async function createLanguageService( // NodeJS: up to 4.9, Node10: since 5.0 (ts.ModuleResolutionKind as any).NodeJs ?? ts.ModuleResolutionKind.Node10; } + if ( !compilerOptions.module || [ @@ -712,6 +713,12 @@ async function createLanguageService( compilerOptions.module = ts.ModuleKind.ESNext; } + if (!compilerOptions.target) { + compilerOptions.target = ts.ScriptTarget.Latest; + } else if (ts.ScriptTarget.ES2015 > compilerOptions.target) { + compilerOptions.target = ts.ScriptTarget.ES2015; + } + // detect which JSX namespace to use (svelte | svelteNative) if not specified or not compatible if (!compilerOptions.jsxFactory || !compilerOptions.jsxFactory.startsWith('svelte')) { //override if we detect svelte-native From 5181ab051f0e635186c1fdf5ea3908fa161ebced Mon Sep 17 00:00:00 2001 From: Lyu Jason Date: Tue, 20 Aug 2024 22:55:45 +0800 Subject: [PATCH 31/36] fix service test. oops some stuff actually don't work in directory with uppercase --- .../language-server/test/plugins/typescript/service.test.ts | 3 ++- packages/language-server/test/plugins/typescript/test-utils.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index c16517557..e0ba1e833 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -73,7 +73,8 @@ describe('service', () => { checkJs: true, strict: true, module: ts.ModuleKind.ESNext, - moduleResolution: ts.ModuleResolutionKind.Node10 + moduleResolution: ts.ModuleResolutionKind.Node10, + target: ts.ScriptTarget.ESNext }); }); diff --git a/packages/language-server/test/plugins/typescript/test-utils.ts b/packages/language-server/test/plugins/typescript/test-utils.ts index 4e6972f57..f615f847f 100644 --- a/packages/language-server/test/plugins/typescript/test-utils.ts +++ b/packages/language-server/test/plugins/typescript/test-utils.ts @@ -106,7 +106,7 @@ export function createVirtualTsSystem(currentDirectory: string): ts.System { ); } - const normalizedPath = normalizePath(toAbsolute(path)); + const normalizedPath = getCanonicalFileName(normalizePath(toAbsolute(path))); return Array.from(virtualFs.keys()).filter((fileName) => fileName.startsWith(normalizedPath) ); From f105638ab5713abd9d05af5119c25a70c9898beb Mon Sep 17 00:00:00 2001 From: Lyu Jason Date: Tue, 20 Aug 2024 22:56:39 +0800 Subject: [PATCH 32/36] no svelte input diagnostics --- .../plugins/typescript/LSAndTSDocResolver.ts | 6 +- .../src/plugins/typescript/service.ts | 62 +++++++++++++++++-- packages/language-server/src/server.ts | 5 +- .../test/plugins/typescript/service.test.ts | 3 +- 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index b5452a6b7..1c3e542aa 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -1,6 +1,6 @@ import { dirname, join } from 'path'; import ts from 'typescript'; -import { RelativePattern, TextDocumentContentChangeEvent } from 'vscode-languageserver'; +import { PublishDiagnosticsParams, RelativePattern, TextDocumentContentChangeEvent } from 'vscode-languageserver'; import { Document, DocumentManager } from '../../lib/documents'; import { LSConfigManager } from '../../ls-config'; import { @@ -37,6 +37,7 @@ interface LSAndTSDocResolverOptions { tsconfigPath?: string; onProjectReloaded?: () => void; + reportConfigError?: (diagnostic: PublishDiagnosticsParams) => void; watch?: boolean; tsSystem?: ts.System; watchDirectory?: (patterns: RelativePattern[]) => void; @@ -121,7 +122,8 @@ export class LSAndTSDocResolver { watchDirectory: this.options?.watchDirectory ? this.watchDirectory.bind(this) : undefined, - nonRecursiveWatchPattern: this.options?.nonRecursiveWatchPattern + nonRecursiveWatchPattern: this.options?.nonRecursiveWatchPattern, + reportConfigError: this.options?.reportConfigError }; } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 1762d1180..07be8d2ab 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -1,6 +1,10 @@ import { dirname, join, resolve } from 'path'; import ts from 'typescript'; -import { RelativePattern, TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; +import { + PublishDiagnosticsParams, + RelativePattern, + TextDocumentContentChangeEvent +} from 'vscode-languageserver-protocol'; import { getPackageInfo, importSvelte } from '../../importPackage'; import { Document } from '../../lib/documents'; import { configLoader } from '../../lib/documents/configLoader'; @@ -15,11 +19,7 @@ import { } from '../../utils'; import { DocumentSnapshot, SvelteSnapshotOptions } from './DocumentSnapshot'; import { createSvelteModuleLoader } from './module-loader'; -import { - GlobalSnapshotsManager, - ignoredBuildDirectories, - SnapshotManager -} from './SnapshotManager'; +import { GlobalSnapshotsManager, SnapshotManager } from './SnapshotManager'; import { ensureRealSvelteFilePath, findTsConfigPath, @@ -134,6 +134,7 @@ export interface LanguageServiceDocumentContext { notifyExceedSizeLimit: (() => void) | undefined; extendedConfigCache: Map; onProjectReloaded: ((configFileNames: string[]) => void) | undefined; + reportConfigError: ((diagnostics: PublishDiagnosticsParams) => void) | undefined; watchTsConfig: boolean; tsSystem: ts.System; projectService: ProjectService | undefined; @@ -736,6 +737,21 @@ async function createLanguageService( } } + const svelteConfigDiagnostics = checkSvelteInput(parsedConfig); + if (docContext.reportConfigError) { + docContext.reportConfigError({ + uri: pathToUrl(tsconfigPath), + diagnostics: svelteConfigDiagnostics.map((d) => ({ + message: d.messageText as string, + range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, + severity: ts.DiagnosticCategory.Error, + source: 'svelte' + })) + }); + } else { + parsedConfig.errors.push(...svelteConfigDiagnostics); + } + return { ...parsedConfig, fileNames: parsedConfig.fileNames.map(normalizePath), @@ -744,6 +760,33 @@ async function createLanguageService( }; } + function checkSvelteInput(config: ts.ParsedCommandLine) { + if (!tsconfigPath || config.raw.references || config.raw.files) { + return []; + } + + const svelteFiles = config.fileNames.filter(isSvelteFilePath); + if (svelteFiles.length > 0) { + return []; + } + const { include, exclude } = config.raw; + const inputText = JSON.stringify(include); + const excludeText = JSON.stringify(exclude); + const svelteConfigDiagnostics: ts.Diagnostic[] = [ + { + category: ts.DiagnosticCategory.Error, + code: 0, + file: undefined, + start: undefined, + length: undefined, + messageText: `No svelte input files were found in config file '${tsconfigPath}'. Specified 'include' paths were '${inputText}' and 'exclude' paths were '${excludeText}'`, + source: 'svelte' + } + ]; + + return svelteConfigDiagnostics; + } + function parseDefaultCompilerOptions() { let configJson = { compilerOptions: { @@ -870,6 +913,7 @@ async function createLanguageService( } docContext.onProjectReloaded?.([fileName]); + docContext.reportConfigError?.({ uri: pathToUrl(fileName), diagnostics: [] }); } function updateIfDirty() { @@ -1203,6 +1247,12 @@ function createWatchDependedConfigCallback(docContext: LanguageServiceDocumentCo return; } + const getCanonicalFileName = createGetCanonicalFileName( + docContext.tsSystem.useCaseSensitiveFileNames + ); + + docContext.extendedConfigCache.delete(getCanonicalFileName(fileName)); + // rely on TypeScript internal behavior so delete both just in case docContext.extendedConfigCache.delete(fileName); const reloadingConfigs: string[] = []; diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 3fffda855..d19e56dcc 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -199,7 +199,10 @@ export function startServer(options?: LSOptions) { onProjectReloaded: refreshCrossFilesSemanticFeatures, watch: true, nonRecursiveWatchPattern, - watchDirectory: (patterns) => watchDirectory(patterns) + watchDirectory: (patterns) => watchDirectory(patterns), + reportConfigError(diagnostic) { + connection?.sendDiagnostics(diagnostic); + } }), normalizedWorkspaceUris, docManager diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index e0ba1e833..22589871c 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -34,7 +34,8 @@ describe('service', () => { onProjectReloaded: undefined, projectService: undefined, nonRecursiveWatchPattern: undefined, - watchDirectory: undefined + watchDirectory: undefined, + reportConfigError: undefined }; return { virtualSystem, lsDocumentContext, rootUris }; From 0b0f2a4929660a9b779b5eff02fc417f3e599d26 Mon Sep 17 00:00:00 2001 From: Lyu Jason Date: Tue, 20 Aug 2024 23:08:41 +0800 Subject: [PATCH 33/36] format --- .../src/plugins/typescript/LSAndTSDocResolver.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index 1c3e542aa..3b0a76c06 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -1,6 +1,10 @@ import { dirname, join } from 'path'; import ts from 'typescript'; -import { PublishDiagnosticsParams, RelativePattern, TextDocumentContentChangeEvent } from 'vscode-languageserver'; +import { + PublishDiagnosticsParams, + RelativePattern, + TextDocumentContentChangeEvent +} from 'vscode-languageserver'; import { Document, DocumentManager } from '../../lib/documents'; import { LSConfigManager } from '../../ls-config'; import { From eec3b2bd2a864dc2396a9b101e417b604ae24ba8 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 21 Aug 2024 12:03:51 +0800 Subject: [PATCH 34/36] show config errors in svelte-check Only editor shows the config error of referenced project so don't push it to the configErrors --- .../src/plugins/typescript/service.ts | 34 ++++++-------- packages/language-server/src/svelte-check.ts | 47 ++++++++++++------- packages/svelte-check/src/writers.ts | 21 ++++++--- 3 files changed, 59 insertions(+), 43 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 07be8d2ab..d790e4e6a 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -112,7 +112,7 @@ const configFileForOpenFiles = new FileMap(); const pendingReloads = new FileSet(); const documentRegistries = new Map(); const pendingForAllServices = new Set>(); -const projectReferenceInfo = new FileMap(); +const parsedTsConfigInfo = new FileMap(); /** * For testing only: Reset the cache for services. @@ -121,7 +121,7 @@ const projectReferenceInfo = new FileMap(); */ export function __resetCache() { services.clear(); - projectReferenceInfo.clear(); + parsedTsConfigInfo.clear(); serviceSizeMap.clear(); configFileForOpenFiles.clear(); } @@ -153,7 +153,7 @@ export async function getService( const fileExistsWithCache = (fileName: string) => { return ( - (projectReferenceInfo.has(fileName) && !pendingReloads.has(fileName)) || + (parsedTsConfigInfo.has(fileName) && !pendingReloads.has(fileName)) || docContext.tsSystem.fileExists(fileName) ); }; @@ -286,7 +286,7 @@ export async function getServiceForTsconfig( if (reloading || !services.has(tsconfigPathOrWorkspacePath)) { if (reloading) { Logger.log('Reloading ts service at ', tsconfigPath, ' due to config updated'); - projectReferenceInfo.delete(tsconfigPath); + parsedTsConfigInfo.delete(tsconfigPath); } else { Logger.log('Initialize new ts service at ', tsconfigPath); } @@ -313,9 +313,8 @@ async function createLanguageService( ): Promise { const { tsSystem } = docContext; - const configErrors: ts.Diagnostic[] = []; const projectConfig = getParsedConfig(); - const { options: compilerOptions, raw } = projectConfig; + const { options: compilerOptions, raw, errors: configErrors } = projectConfig; const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); watchWildCardDirectories(projectConfig); @@ -442,7 +441,7 @@ async function createLanguageService( parsedCommandLine: ts.ParsedCommandLine, configFileName: string ) { - const cached = configFileName ? projectReferenceInfo.get(configFileName) : undefined; + const cached = configFileName ? parsedTsConfigInfo.get(configFileName) : undefined; if (cached?.snapshotManager) { return cached.snapshotManager; } @@ -598,7 +597,7 @@ async function createLanguageService( function scheduleProjectFileUpdate(watcherNewFiles: string[]): void { if (!snapshotManager.areIgnoredFromNewFileWatch(watcherNewFiles)) { scheduleUpdate(); - const info = projectReferenceInfo.get(tsconfigPath); + const info = parsedTsConfigInfo.get(tsconfigPath); if (info) { info.pendingProjectFileUpdate = true; } @@ -608,7 +607,7 @@ async function createLanguageService( return; } for (const ref of projectConfig.projectReferences) { - const config = projectReferenceInfo.get(ref.path); + const config = parsedTsConfigInfo.get(ref.path); if ( config && // handled by the respective service @@ -622,7 +621,7 @@ async function createLanguageService( } function ensureProjectFileUpdates(): void { - const info = projectReferenceInfo.get(tsconfigPath); + const info = parsedTsConfigInfo.get(tsconfigPath); if (!info || !info.pendingProjectFileUpdate) { return; } @@ -738,8 +737,8 @@ async function createLanguageService( } const svelteConfigDiagnostics = checkSvelteInput(parsedConfig); - if (docContext.reportConfigError) { - docContext.reportConfigError({ + if (svelteConfigDiagnostics.length > 0) { + docContext.reportConfigError?.({ uri: pathToUrl(tsconfigPath), diagnostics: svelteConfigDiagnostics.map((d) => ({ message: d.messageText as string, @@ -748,7 +747,6 @@ async function createLanguageService( source: 'svelte' })) }); - } else { parsedConfig.errors.push(...svelteConfigDiagnostics); } @@ -1035,7 +1033,7 @@ async function createLanguageService( } function ensureTsConfigInfoUpToDate(configFilePath: string) { - const cached = projectReferenceInfo.get(configFilePath); + const cached = parsedTsConfigInfo.get(configFilePath); if (cached !== undefined) { ensureFilesForConfigUpdates(cached); return cached; @@ -1043,7 +1041,7 @@ async function createLanguageService( const content = tsSystem.fileExists(configFilePath) && tsSystem.readFile(configFilePath); if (!content) { - projectReferenceInfo.set(configFilePath, null); + parsedTsConfigInfo.set(configFilePath, null); return null; } @@ -1091,10 +1089,6 @@ async function createLanguageService( parsedCommandLine.options.allowNonTsExtensions = true; - if (parsedCommandLine.errors.length) { - configErrors.push(...parsedCommandLine.errors); - } - const snapshotManager = createSnapshotManager(parsedCommandLine, configFilePath); const tsconfigInfo: TsConfigInfo = { @@ -1104,7 +1098,7 @@ async function createLanguageService( configFilePath, extendedConfigPaths }; - projectReferenceInfo.set(configFilePath, tsconfigInfo); + parsedTsConfigInfo.set(configFilePath, tsconfigInfo); watchConfigFiles(extendedConfigPaths, parsedCommandLine); diff --git a/packages/language-server/src/svelte-check.ts b/packages/language-server/src/svelte-check.ts index 57ab91586..de8e54f56 100644 --- a/packages/language-server/src/svelte-check.ts +++ b/packages/language-server/src/svelte-check.ts @@ -18,6 +18,7 @@ import { JSOrTSDocumentSnapshot } from './plugins/typescript/DocumentSnapshot'; import { isInGeneratedCode } from './plugins/typescript/features/utils'; import { convertRange, getDiagnosticTag, mapSeverity } from './plugins/typescript/utils'; import { pathToUrl, urlToPath } from './utils'; +import { groupBy } from 'lodash'; export type SvelteCheckDiagnosticSource = 'js' | 'css' | 'svelte'; @@ -188,10 +189,36 @@ export class SvelteCheck { private async getDiagnosticsForTsconfig(tsconfigPath: string) { const lsContainer = await this.getLSContainer(tsconfigPath); + const map = (diagnostic: ts.Diagnostic, range?: Range): Diagnostic => { + const file = diagnostic.file; + range ??= file + ? convertRange( + { positionAt: file.getLineAndCharacterOfPosition.bind(file) }, + diagnostic + ) + : { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }; - const noInputsFoundError = lsContainer.configErrors?.find((e) => e.code === 18003); - if (noInputsFoundError) { - throw new Error(noInputsFoundError.messageText.toString()); + return { + range: range, + severity: mapSeverity(diagnostic.category), + source: diagnostic.source, + message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'), + code: diagnostic.code, + tags: getDiagnosticTag(diagnostic) + }; + }; + + if (lsContainer.configErrors) { + const grouped = groupBy( + lsContainer.configErrors, + (error) => error.file?.fileName ?? tsconfigPath + ); + + return Object.entries(grouped).map(([filePath, errors]) => ({ + filePath, + text: '', + diagnostics: errors.map((diagnostic) => map(diagnostic)) + })); } const lang = lsContainer.getService(); @@ -219,20 +246,6 @@ export class SvelteCheck { | undefined; const isKitFile = snapshot?.kitFile ?? false; const diagnostics: Diagnostic[] = []; - const map = (diagnostic: ts.Diagnostic, range?: Range) => ({ - range: - range ?? - convertRange( - { positionAt: file.getLineAndCharacterOfPosition.bind(file) }, - diagnostic - ), - severity: mapSeverity(diagnostic.category), - source: diagnostic.source, - message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'), - code: diagnostic.code, - tags: getDiagnosticTag(diagnostic) - }); - if (!skipDiagnosticsForFile) { const originalDiagnostics = [ ...lang.getSyntacticDiagnostics(file.fileName), diff --git a/packages/svelte-check/src/writers.ts b/packages/svelte-check/src/writers.ts index 37c6823b8..af1fd89d8 100644 --- a/packages/svelte-check/src/writers.ts +++ b/packages/svelte-check/src/writers.ts @@ -57,14 +57,9 @@ export class HumanFriendlyWriter implements Writer { `${workspaceDir}${sep}${pc.green(filename)}:${line + 1}:${character + 1}\n` ); - // Show some context around diagnostic range - const codePrevLine = this.getLine(diagnostic.range.start.line - 1, text); - const codeLine = this.getCodeLine(diagnostic, text); - const codeNextLine = this.getLine(diagnostic.range.end.line + 1, text); - const code = codePrevLine + codeLine + codeNextLine; - let msg; if (this.isVerbose) { + const code = this.formatRelatedCode(diagnostic, text); msg = `${diagnostic.message} ${source}\n${pc.cyan(code)}`; } else { msg = `${diagnostic.message} ${source}`; @@ -80,6 +75,20 @@ export class HumanFriendlyWriter implements Writer { }); } + private formatRelatedCode(diagnostic: Diagnostic, text: string) { + if (!text) { + return ''; + } + + // Show some context around diagnostic range + const codePrevLine = this.getLine(diagnostic.range.start.line - 1, text); + const codeLine = this.getCodeLine(diagnostic, text); + const codeNextLine = this.getLine(diagnostic.range.end.line + 1, text); + const code = codePrevLine + codeLine + codeNextLine; + + return code; + } + private getCodeLine(diagnostic: Diagnostic, text: string) { const startOffset = offsetAt(diagnostic.range.start, text); const endOffset = offsetAt(diagnostic.range.end, text); From fd22d6a8ee1e5327ca425ad1b6e7099da79f918a Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 21 Aug 2024 12:33:04 +0800 Subject: [PATCH 35/36] tweak word, test --- .../src/plugins/typescript/service.ts | 7 +- .../test/plugins/typescript/service.test.ts | 85 +++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index d790e4e6a..697493048 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -1,4 +1,4 @@ -import { dirname, join, resolve } from 'path'; +import { dirname, join, resolve, basename } from 'path'; import ts from 'typescript'; import { PublishDiagnosticsParams, @@ -777,7 +777,10 @@ async function createLanguageService( file: undefined, start: undefined, length: undefined, - messageText: `No svelte input files were found in config file '${tsconfigPath}'. Specified 'include' paths were '${inputText}' and 'exclude' paths were '${excludeText}'`, + messageText: + `No svelte input files were found in config file '${tsconfigPath}'. ` + + `Did you forget to add svelte files to the "include" in your ${basename(tsconfigPath)}? ` + + `Specified 'include' paths were '${inputText}' and 'exclude' paths were '${excludeText}'`, source: 'svelte' } ]; diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index 22589871c..50c793c6f 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -79,6 +79,91 @@ describe('service', () => { }); }); + it('errors if tsconfig matches no svelte files', async () => { + const dirPath = getRandomVirtualDirPath(testDir); + const { virtualSystem, lsDocumentContext, rootUris } = setup(); + + virtualSystem.readDirectory = () => [path.join(dirPath, 'random.ts')]; + + virtualSystem.writeFile( + path.join(dirPath, 'tsconfig.json'), + JSON.stringify({ + include: ['**/*.ts'] + }) + ); + + virtualSystem.writeFile( + path.join(dirPath, 'random.svelte'), + '' + ); + + let called = false; + await getService(path.join(dirPath, 'random.svelte'), rootUris, { + ...lsDocumentContext, + reportConfigError: (message) => { + called = true; + assert.equal(message.uri, pathToUrl(path.join(dirPath, 'tsconfig.json'))); + } + }); + assert.ok(called); + }); + + it('do not errors if referenced tsconfig matches no svelte files', async () => { + const dirPath = getRandomVirtualDirPath(testDir); + const { virtualSystem, lsDocumentContext, rootUris } = setup(); + + const tsPattern = '**/*.ts'; + const sveltePattern = '**/*.svelte'; + virtualSystem.readDirectory = (_path, _extensions, _excludes, include) => { + return include?.[0] === tsPattern + ? [path.join(dirPath, 'random.ts')] + : include?.[0] === sveltePattern + ? [path.join(dirPath, 'random.svelte')] + : []; + }; + + virtualSystem.writeFile( + path.join(dirPath, 'tsconfig.json'), + JSON.stringify({ + include: [], + references: [{ path: './tsconfig_node.json' }, { path: './tsconfig_web.json' }] + }) + ); + + virtualSystem.writeFile( + path.join(dirPath, 'tsconfig_node.json'), + JSON.stringify({ + include: [tsPattern] + }) + ); + + virtualSystem.writeFile( + path.join(dirPath, 'tsconfig_web.json'), + JSON.stringify({ + include: [sveltePattern] + }) + ); + + virtualSystem.writeFile( + path.join(dirPath, 'random.svelte'), + '' + ); + + let called = false; + const lsContainer = await getService(path.join(dirPath, 'random.svelte'), rootUris, { + ...lsDocumentContext, + reportConfigError: () => { + called = true; + } + }); + + assert.equal( + normalizePath(path.join(dirPath, 'tsconfig_web.json')), + lsContainer.tsconfigPath + ); + assert.equal(called, false, 'expected not to call reportConfigError'); + }); + it('can loads default tsconfig', async () => { const dirPath = getRandomVirtualDirPath(testDir); const { lsDocumentContext, rootUris } = setup(); From dc5662d60f35a4896e65e2c4bceafc8e649fc3ca Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Wed, 21 Aug 2024 12:42:10 +0800 Subject: [PATCH 36/36] single quote --- packages/language-server/src/plugins/typescript/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 697493048..8f5720bb0 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -779,7 +779,7 @@ async function createLanguageService( length: undefined, messageText: `No svelte input files were found in config file '${tsconfigPath}'. ` + - `Did you forget to add svelte files to the "include" in your ${basename(tsconfigPath)}? ` + + `Did you forget to add svelte files to the 'include' in your ${basename(tsconfigPath)}? ` + `Specified 'include' paths were '${inputText}' and 'exclude' paths were '${excludeText}'`, source: 'svelte' }