diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index 4cbde00b8..74f822e6c 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -7,8 +7,7 @@ import { ensureRealSvelteFilePath, getExtensionFromScriptKind, isSvelteFilePath, - isVirtualSvelteFilePath, - toVirtualSvelteFilePath + isVirtualSvelteFilePath } from './utils'; const CACHE_KEY_SEPARATOR = ':::'; @@ -89,8 +88,6 @@ class ModuleResolutionCache { } class ImpliedNodeFormatResolver { - private alreadyResolved = new FileMap>(); - constructor(private readonly tsSystem: ts.System) {} resolve( @@ -106,39 +103,17 @@ class ImpliedNodeFormatResolver { let mode: ReturnType = undefined; if (sourceFile) { - this.cacheImpliedNodeFormat(sourceFile, compilerOptions); mode = ts.getModeForResolutionAtIndex(sourceFile, importIdxInFile, compilerOptions); } return mode; } - private cacheImpliedNodeFormat(sourceFile: ts.SourceFile, compilerOptions: ts.CompilerOptions) { - if (!sourceFile.impliedNodeFormat && isSvelteFilePath(sourceFile.fileName)) { - // impliedNodeFormat is not set for Svelte files, because the TS function which - // calculates this works with a fixed set of file extensions, - // which .svelte is obv not part of. Make it work by faking a TS file. - if (!this.alreadyResolved.has(sourceFile.fileName)) { - sourceFile.impliedNodeFormat = ts.getImpliedNodeFormatForFile( - toVirtualSvelteFilePath(sourceFile.fileName) as any, - undefined, - this.tsSystem, - compilerOptions - ); - this.alreadyResolved.set(sourceFile.fileName, sourceFile.impliedNodeFormat); - } else { - sourceFile.impliedNodeFormat = this.alreadyResolved.get(sourceFile.fileName); - } - } - } - resolveForTypeReference( entry: string | ts.FileReference, - sourceFile: ts.SourceFile | undefined, - compilerOptions: ts.CompilerOptions + sourceFile: ts.SourceFile | undefined ) { let mode = undefined; if (sourceFile) { - this.cacheImpliedNodeFormat(sourceFile, compilerOptions); mode = ts.getModeForFileReference(entry, sourceFile?.impliedNodeFormat); } return mode; @@ -315,8 +290,7 @@ export function createSvelteModuleLoader( const entry = getTypeReferenceResolutionName(typeDirectiveName); const mode = impliedNodeFormatResolver.resolveForTypeReference( entry, - containingSourceFile, - options + containingSourceFile ); const key = `${entry}|${mode}`; diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 3c295129a..a45f04ecd 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -26,7 +26,8 @@ import { findTsConfigPath, getNearestWorkspaceUri, hasTsExtensions, - isSvelteFilePath + isSvelteFilePath, + toVirtualSvelteFilePath } from './utils'; import { createProject, ProjectService } from './serviceCache'; import { internalHelpers } from 'svelte2tsx'; @@ -974,14 +975,19 @@ async function createLanguageService( } const oldProgram = project?.program; - const program = languageService.getProgram(); + let program: ts.Program | undefined; + try { + program = languageService.getProgram(); + } finally { + // mark as clean even if the update fails, at least we can still try again next time there is a change + dirty = false; + } svelteModuleLoader.clearPendingInvalidations(); if (project) { project.program = program; } - dirty = false; compilerHost = undefined; if (!skipSvelteInputCheck) { @@ -1376,38 +1382,93 @@ function getOrCreateDocumentRegistry( registry = ts.createDocumentRegistry(useCaseSensitiveFileNames, currentDirectory); - // impliedNodeFormat is always undefined when the svelte source file is created - // We might patched it later but the registry doesn't know about it - const releaseDocumentWithKey = registry.releaseDocumentWithKey; - registry.releaseDocumentWithKey = ( + const acquireDocumentWithKey = registry.acquireDocumentWithKey; + registry.acquireDocumentWithKey = ( + fileName: string, path: ts.Path, + compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, key: ts.DocumentRegistryBucketKey, - scriptKind: ts.ScriptKind, - impliedNodeFormat?: ts.ResolutionMode + scriptSnapshot: ts.IScriptSnapshot, + version: string, + scriptKind?: ts.ScriptKind, + sourceFileOptions?: ts.CreateSourceFileOptions | ts.ScriptTarget ) => { - if (isSvelteFilePath(path)) { - releaseDocumentWithKey(path, key, scriptKind, undefined); - return; - } + ensureImpliedNodeFormat(compilationSettingsOrHost, fileName, sourceFileOptions); - releaseDocumentWithKey(path, key, scriptKind, impliedNodeFormat); + return acquireDocumentWithKey( + fileName, + path, + compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions + ); }; - registry.releaseDocument = ( + const updateDocumentWithKey = registry.updateDocumentWithKey; + registry.updateDocumentWithKey = ( fileName: string, - compilationSettings: ts.CompilerOptions, - scriptKind: ts.ScriptKind, - impliedNodeFormat?: ts.ResolutionMode + path: ts.Path, + compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, + key: ts.DocumentRegistryBucketKey, + scriptSnapshot: ts.IScriptSnapshot, + version: string, + scriptKind?: ts.ScriptKind, + sourceFileOptions?: ts.CreateSourceFileOptions | ts.ScriptTarget ) => { - if (isSvelteFilePath(fileName)) { - registry?.releaseDocument(fileName, compilationSettings, scriptKind, undefined); - return; - } + ensureImpliedNodeFormat(compilationSettingsOrHost, fileName, sourceFileOptions); - registry?.releaseDocument(fileName, compilationSettings, scriptKind, impliedNodeFormat); + return updateDocumentWithKey( + fileName, + path, + compilationSettingsOrHost, + key, + scriptSnapshot, + version, + scriptKind, + sourceFileOptions + ); }; documentRegistries.set(key, registry); return registry; + + function ensureImpliedNodeFormat( + compilationSettingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost, + fileName: string, + sourceFileOptions: ts.CreateSourceFileOptions | ts.ScriptTarget | undefined + ) { + const compilationSettings = getCompilationSettings(compilationSettingsOrHost); + const host: ts.MinimalResolutionCacheHost | undefined = + compilationSettingsOrHost === compilationSettings + ? undefined + : (compilationSettingsOrHost as ts.MinimalResolutionCacheHost); + if ( + host && + isSvelteFilePath(fileName) && + typeof sourceFileOptions === 'object' && + !sourceFileOptions.impliedNodeFormat + ) { + const format = ts.getImpliedNodeFormatForFile( + toVirtualSvelteFilePath(fileName), + host?.getCompilerHost?.()?.getModuleResolutionCache?.()?.getPackageJsonInfoCache(), + host, + compilationSettings + ); + + sourceFileOptions.impliedNodeFormat = format; + } + } + + function getCompilationSettings( + settingsOrHost: ts.CompilerOptions | ts.MinimalResolutionCacheHost + ) { + if (typeof settingsOrHost.getCompilationSettings === 'function') { + return (settingsOrHost as ts.MinimalResolutionCacheHost).getCompilationSettings(); + } + return settingsOrHost as ts.CompilerOptions; + } } diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index 360848fad..d2832bc33 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -281,6 +281,57 @@ describe('service', () => { }); }); + it('do not throw when script tag is nuked', async () => { + // testing this because the patch rely on ts implementation details + // and we want to be aware of the changes + + const dirPath = getRandomVirtualDirPath(testDir); + const { virtualSystem, lsDocumentContext, rootUris } = setup(); + + virtualSystem.writeFile( + path.join(dirPath, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + module: 'NodeNext', + moduleResolution: 'NodeNext' + } + }) + ); + + virtualSystem.writeFile( + path.join(dirPath, 'random.svelte'), + '' + ); + virtualSystem.writeFile( + path.join(dirPath, 'random2.svelte'), + '' + ); + + const ls = await getService( + path.join(dirPath, 'random.svelte'), + rootUris, + lsDocumentContext + ); + + const document = new Document(pathToUrl(path.join(dirPath, 'random.svelte')), ''); + document.openedByClient = true; + ls.updateSnapshot(document); + + const document2 = new Document( + pathToUrl(path.join(dirPath, 'random2.svelte')), + virtualSystem.readFile(path.join(dirPath, 'random2.svelte'))! + ); + document.openedByClient = true; + ls.updateSnapshot(document2); + + const lang = ls.getService(); + lang.getProgram(); + + document2.update(' Promise