From 131f78aa805c54bb850710c44fd98f17d448f1d3 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:23:48 +0100 Subject: [PATCH] fix: make sure snapshots are shared per ProjectService (#2614) The ProjectService contains snapshots. Multiple TypeScript plugins may be instantiated for one projectservice. Ensure we share our internal Svelte snapshots between those instances. #2602 --- .../typescript-plugin/src/svelte-snapshots.ts | 160 +++++++++--------- 1 file changed, 76 insertions(+), 84 deletions(-) diff --git a/packages/typescript-plugin/src/svelte-snapshots.ts b/packages/typescript-plugin/src/svelte-snapshots.ts index 438ceb6ca..87cad7305 100644 --- a/packages/typescript-plugin/src/svelte-snapshots.ts +++ b/packages/typescript-plugin/src/svelte-snapshots.ts @@ -269,7 +269,7 @@ export class SvelteSnapshot { } export class SvelteSnapshotManager { - private snapshots = new Map(); + private snapshots: Map; constructor( private typescript: typeof ts, @@ -280,7 +280,9 @@ export class SvelteSnapshotManager { /** undefined if no node_modules with Svelte next to tsconfig.json */ private svelteCompiler: typeof import('svelte/compiler') | undefined ) { - this.patchProjectServiceReadFile(); + this.patchProjectService(); + // @ts-expect-error + this.snapshots = this.projectService[snapshots]; } get(fileName: string) { @@ -321,92 +323,78 @@ export class SvelteSnapshotManager { return snapshot; } - private patchProjectServiceReadFile() { - // @ts-ignore The projectService is shared across some instances, make sure we patch readFile only once - if (!this.projectService.host[onReadSvelteFile]) { - this.logger.log('patching projectService host readFile'); + private patchProjectService() { + // @ts-expect-error The projectService is shared across some instances, make sure we patch only once + if (this.projectService[snapshots]) return; - // @ts-ignore - this.projectService.host[onReadSvelteFile] = []; + this.logger.log('patching projectService'); - const readFile = this.projectService.host.readFile; - this.projectService.host.readFile = (path: string, encoding?: string | undefined) => { - if (!this.configManager.getConfig().enable) { - return readFile(path, encoding); - } + // @ts-expect-error Snapshots are stored on the projectService, so they are shared across all instances + this.snapshots = this.projectService[snapshots] = new Map(); - // The following (very hacky) first two checks make sure that the ambient module definitions - // that tell TS "every import ending with .svelte is a valid module" are removed. - // They exist in svelte2tsx and svelte to make sure that people don't - // get errors in their TS files when importing Svelte files and not using our TS plugin. - // If someone wants to get back the behavior they can add an ambient module definition - // on their own. - const normalizedPath = path.replace(/\\/g, '/'); - if (normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) { - return ''; - } else if (normalizedPath.endsWith('svelte2tsx/svelte-jsx.d.ts')) { - // Remove the dom lib reference to not load these ambient types in case - // the user has a tsconfig.json with different lib settings like in - // https://github.com/sveltejs/language-tools/issues/1733 - const originalText = readFile(path) || ''; - const toReplace = '/// '; - return originalText.replace(toReplace, ' '.repeat(toReplace.length)); - } else if (normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts')) { - let originalText = readFile(path) || ''; - if (!originalText.includes('// -- start svelte-ls-remove --')) { - return originalText; // uses an older version of svelte2tsx or is already patched - } - const startIdx = originalText.indexOf('// -- start svelte-ls-remove --'); - const endIdx = originalText.indexOf('// -- end svelte-ls-remove --'); - originalText = - originalText.substring(0, startIdx) + - ' '.repeat(endIdx - startIdx) + - originalText.substring(endIdx); - return originalText; - } else if (isSvelteFilePath(path)) { - this.logger.debug('Read Svelte file:', path); - const svelteCode = readFile(path) || ''; - const isTsFile = true; // TODO check file contents? TS might be okay with importing ts into js. - let code: string; - let mapper: SourceMapper; - - try { - const result = svelte2tsx(svelteCode, { - filename: path.split('/').pop(), - isTsFile, - mode: 'ts', - typingsNamespace: this.svelteOptions.namespace, - // Don't search for compiler from current path - could be a different one from which we have loaded the svelte2tsx globals - parse: this.svelteCompiler?.parse, - version: this.svelteCompiler?.VERSION - }); - code = result.code; - mapper = new SourceMapper(result.map.mappings); - this.logger.log('Successfully read Svelte file contents of', path); - } catch (e) { - this.logger.log('Error loading Svelte file:', path, ' Using fallback.'); - this.logger.debug('Error:', e); - // Return something either way, else "X is not a module" errors will appear - // in the TS files that use this file. - code = 'export default class extends Svelte2TsxComponent {}'; - mapper = new SourceMapper(''); - } - - // @ts-ignore - this.projectService.host[onReadSvelteFile].forEach((listener) => - listener(path, svelteCode, isTsFile, mapper) - ); + const readFile = this.projectService.host.readFile; + this.projectService.host.readFile = (path: string, encoding?: string | undefined) => { + if (!this.configManager.getConfig().enable) { + return readFile(path, encoding); + } - return code; - } else { - return readFile(path, encoding); + // The following (very hacky) first two checks make sure that the ambient module definitions + // that tell TS "every import ending with .svelte is a valid module" are removed. + // They exist in svelte2tsx and svelte to make sure that people don't + // get errors in their TS files when importing Svelte files and not using our TS plugin. + // If someone wants to get back the behavior they can add an ambient module definition + // on their own. + const normalizedPath = path.replace(/\\/g, '/'); + if (normalizedPath.endsWith('node_modules/svelte/types/runtime/ambient.d.ts')) { + return ''; + } else if (normalizedPath.endsWith('svelte2tsx/svelte-jsx.d.ts')) { + // Remove the dom lib reference to not load these ambient types in case + // the user has a tsconfig.json with different lib settings like in + // https://github.com/sveltejs/language-tools/issues/1733 + const originalText = readFile(path) || ''; + const toReplace = '/// '; + return originalText.replace(toReplace, ' '.repeat(toReplace.length)); + } else if (normalizedPath.endsWith('svelte2tsx/svelte-shims.d.ts')) { + let originalText = readFile(path) || ''; + if (!originalText.includes('// -- start svelte-ls-remove --')) { + return originalText; // uses an older version of svelte2tsx or is already patched + } + const startIdx = originalText.indexOf('// -- start svelte-ls-remove --'); + const endIdx = originalText.indexOf('// -- end svelte-ls-remove --'); + originalText = + originalText.substring(0, startIdx) + + ' '.repeat(endIdx - startIdx) + + originalText.substring(endIdx); + return originalText; + } else if (isSvelteFilePath(path)) { + this.logger.debug('Read Svelte file:', path); + const svelteCode = readFile(path) || ''; + const isTsFile = true; // TODO check file contents? TS might be okay with importing ts into js. + let code: string; + let mapper: SourceMapper; + + try { + const result = svelte2tsx(svelteCode, { + filename: path.split('/').pop(), + isTsFile, + mode: 'ts', + typingsNamespace: this.svelteOptions.namespace, + // Don't search for compiler from current path - could be a different one from which we have loaded the svelte2tsx globals + parse: this.svelteCompiler?.parse, + version: this.svelteCompiler?.VERSION + }); + code = result.code; + mapper = new SourceMapper(result.map.mappings); + this.logger.log('Successfully read Svelte file contents of', path); + } catch (e) { + this.logger.log('Error loading Svelte file:', path, ' Using fallback.'); + this.logger.debug('Error:', e); + // Return something either way, else "X is not a module" errors will appear + // in the TS files that use this file. + code = 'export default class extends Svelte2TsxComponent {}'; + mapper = new SourceMapper(''); } - }; - } - // @ts-ignore - this.projectService.host[onReadSvelteFile].push( - (path: string, svelteCode: string, isTsFile: boolean, mapper: SourceMapper) => { const canonicalFilePath = this.projectService.toCanonicalFileName(path); const existingSnapshot = this.snapshots.get(canonicalFilePath); if (existingSnapshot) { @@ -424,9 +412,13 @@ export class SvelteSnapshotManager { ) ); } + + return code; + } else { + return readFile(path, encoding); } - ); + }; } } -const onReadSvelteFile = Symbol('sveltePluginPatchSymbol'); +const snapshots = Symbol('sveltePluginPatchSymbol');