diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index 505f9b5d7..3ce2bd1da 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -19,7 +19,8 @@ "license": "MIT", "devDependencies": { "@types/node": "^16.0.0", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "svelte": "^3.57.0" }, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.14", diff --git a/packages/typescript-plugin/src/config-manager.ts b/packages/typescript-plugin/src/config-manager.ts index 82f046598..4efd9a019 100644 --- a/packages/typescript-plugin/src/config-manager.ts +++ b/packages/typescript-plugin/src/config-manager.ts @@ -3,6 +3,7 @@ import { EventEmitter } from 'events'; const configurationEventName = 'configuration-changed'; export interface Configuration { + global?: boolean; enable: boolean; /** Skip the Svelte detection and assume this is a Svelte project */ assumeIsSvelteProject: boolean; @@ -19,15 +20,22 @@ export class ConfigManager { this.emitter.on(configurationEventName, listener); } + removeConfigurationChangeListener(listener: (config: Configuration) => void) { + this.emitter.off(configurationEventName, listener); + } + isConfigChanged(config: Configuration) { // right now we only care about enable return config.enable !== this.config.enable; } updateConfigFromPluginConfig(config: Configuration) { + const shouldWaitForConfigRequest = config.global == true; + const enable = config.enable ?? !shouldWaitForConfigRequest; this.config = { ...this.config, - ...config + ...config, + enable }; this.emitter.emit(configurationEventName, config); } diff --git a/packages/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts index 3c85949ae..7da10c273 100644 --- a/packages/typescript-plugin/src/index.ts +++ b/packages/typescript-plugin/src/index.ts @@ -6,17 +6,18 @@ import { SvelteSnapshotManager } from './svelte-snapshots'; import type ts from 'typescript/lib/tsserverlibrary'; import { ConfigManager, Configuration } from './config-manager'; import { ProjectSvelteFilesManager } from './project-svelte-files'; -import { getConfigPathForProject, hasNodeModule } from './utils'; +import { getConfigPathForProject, isSvelteProject } from './utils'; function init(modules: { typescript: typeof ts }): ts.server.PluginModule { const configManager = new ConfigManager(); let resolvedSvelteTsxFiles: string[] | undefined; + const isSvelteProjectCache = new Map(); function create(info: ts.server.PluginCreateInfo) { const logger = new Logger(info.project.projectService.logger); if ( !(info.config as Configuration)?.assumeIsSvelteProject && - !isSvelteProject(info.project.getCompilerOptions()) + !isSvelteProjectWithCache(info.project) ) { logger.log('Detected that this is not a Svelte project, abort patching TypeScript'); return info.languageService; @@ -126,7 +127,7 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { ) : undefined; - patchModuleLoader( + const moduleLoaderDisposable = patchModuleLoader( logger, snapshotManager, modules.typescript, @@ -135,7 +136,7 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { configManager ); - configManager.onConfigurationChanged(() => { + const updateProjectWhenConfigChanges = () => { // enabling/disabling the plugin means TS has to recompute stuff // don't clear semantic cache here // typescript now expected the program updates to be completely in their control @@ -147,7 +148,8 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { if (projectSvelteFilesManager) { info.project.updateGraph(); } - }); + }; + configManager.onConfigurationChanged(updateProjectWhenConfigChanges); return decorateLanguageService( info.languageService, @@ -156,12 +158,16 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { configManager, info, modules.typescript, - () => projectSvelteFilesManager?.dispose() + () => { + projectSvelteFilesManager?.dispose(); + configManager.removeConfigurationChangeListener(updateProjectWhenConfigChanges); + moduleLoaderDisposable.dispose(); + } ); } function getExternalFiles(project: ts.server.Project) { - if (!isSvelteProject(project.getCompilerOptions()) || !configManager.getConfig().enable) { + if (!isSvelteProjectWithCache(project) || !configManager.getConfig().enable) { return []; } @@ -218,9 +224,15 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { return svelteTsxFiles; } - function isSvelteProject(compilerOptions: ts.CompilerOptions) { - // Add more checks like "no Svelte file found" or "no config file found"? - return hasNodeModule(compilerOptions, 'svelte'); + function isSvelteProjectWithCache(project: ts.server.Project) { + const cached = isSvelteProjectCache.get(project.getProjectName()); + if (cached !== undefined) { + return cached; + } + + const result = !!isSvelteProject(project); + isSvelteProjectCache.set(project.getProjectName(), result); + return result; } function onConfigurationChanged(config: Configuration) { diff --git a/packages/typescript-plugin/src/language-service/sveltekit.ts b/packages/typescript-plugin/src/language-service/sveltekit.ts index c4fc800f0..5fec0bebe 100644 --- a/packages/typescript-plugin/src/language-service/sveltekit.ts +++ b/packages/typescript-plugin/src/language-service/sveltekit.ts @@ -1,6 +1,6 @@ import type ts from 'typescript/lib/tsserverlibrary'; import { Logger } from '../logger'; -import { hasNodeModule } from '../utils'; +import { getProjectDirectory, hasNodeModule } from '../utils'; import { InternalHelpers, internalHelpers } from 'svelte2tsx'; type _ts = typeof ts; @@ -531,7 +531,8 @@ function getProxiedLanguageService(info: ts.server.PluginCreateInfo, ts: _ts, lo return cachedProxiedLanguageService ?? undefined; } - if (!hasNodeModule(info.project.getCompilerOptions(), '@sveltejs/kit')) { + const projectDirectory = getProjectDirectory(info.project); + if (projectDirectory && !hasNodeModule(projectDirectory, '@sveltejs/kit')) { // Not a SvelteKit project, do nothing cache.set(info, null); return; diff --git a/packages/typescript-plugin/src/module-loader.ts b/packages/typescript-plugin/src/module-loader.ts index 6da7c5cc9..cc2003cba 100644 --- a/packages/typescript-plugin/src/module-loader.ts +++ b/packages/typescript-plugin/src/module-loader.ts @@ -101,7 +101,7 @@ export function patchModuleLoader( lsHost: ts.LanguageServiceHost, project: ts.server.Project, configManager: ConfigManager -): void { +): { dispose: () => void } { const svelteSys = createSvelteSys(typescript, logger); const moduleCache = new ModuleResolutionCache(project.projectService); const origResolveModuleNames = lsHost.resolveModuleNames?.bind(lsHost); @@ -120,9 +120,17 @@ export function patchModuleLoader( return origRemoveFile(info, fileExists, detachFromProject); }; - configManager.onConfigurationChanged(() => { + const onConfigChanged = () => { moduleCache.clear(); - }); + }; + configManager.onConfigurationChanged(onConfigChanged); + + return { + dispose() { + configManager.removeConfigurationChangeListener(onConfigChanged); + moduleCache.clear(); + } + }; function resolveModuleNames( moduleNames: string[], diff --git a/packages/typescript-plugin/src/project-svelte-files.ts b/packages/typescript-plugin/src/project-svelte-files.ts index 58b315b30..e9cea747e 100644 --- a/packages/typescript-plugin/src/project-svelte-files.ts +++ b/packages/typescript-plugin/src/project-svelte-files.ts @@ -26,14 +26,14 @@ export class ProjectSvelteFilesManager { private readonly snapshotManager: SvelteSnapshotManager, private readonly logger: Logger, private parsedCommandLine: ts.ParsedCommandLine, - configManager: ConfigManager + private readonly configManager: ConfigManager ) { if (configManager.getConfig().enable) { this.setupWatchers(); this.updateProjectSvelteFiles(); } - configManager.onConfigurationChanged(this.onConfigChanged.bind(this)); + configManager.onConfigurationChanged(this.onConfigChanged); ProjectSvelteFilesManager.instances.set(project.getProjectName(), this); } @@ -162,7 +162,7 @@ export class ProjectSvelteFilesManager { .map(this.typescript.server.toNormalizedPath); } - private onConfigChanged(config: Configuration) { + private onConfigChanged = (config: Configuration) => { this.disposeWatchers(); this.clearProjectFile(); @@ -170,7 +170,7 @@ export class ProjectSvelteFilesManager { this.setupWatchers(); this.updateProjectSvelteFiles(); } - } + }; private removeFileFromProject(file: string, exists = true) { const info = this.project.getScriptInfo(file); @@ -198,6 +198,8 @@ export class ProjectSvelteFilesManager { // - and because the project is closed, `project.removeFile` will result in an error this.projectFileToOriginalCasing.clear(); + this.configManager.removeConfigurationChangeListener(this.onConfigChanged); + ProjectSvelteFilesManager.instances.delete(this.project.getProjectName()); } } diff --git a/packages/typescript-plugin/src/svelte-sys.ts b/packages/typescript-plugin/src/svelte-sys.ts index ca0f60ede..d530e0b47 100644 --- a/packages/typescript-plugin/src/svelte-sys.ts +++ b/packages/typescript-plugin/src/svelte-sys.ts @@ -1,4 +1,4 @@ -import type ts from 'typescript'; +import type ts from 'typescript/lib/tsserverlibrary'; import { Logger } from './logger'; import { ensureRealSvelteFilePath, isVirtualSvelteFilePath, toRealSvelteFilePath } from './utils'; diff --git a/packages/typescript-plugin/src/utils.ts b/packages/typescript-plugin/src/utils.ts index 3cdefee9b..92b0a58c6 100644 --- a/packages/typescript-plugin/src/utils.ts +++ b/packages/typescript-plugin/src/utils.ts @@ -1,5 +1,6 @@ import type ts from 'typescript/lib/tsserverlibrary'; import { SvelteSnapshot } from './svelte-snapshots'; +import { dirname, join } from 'path'; type _ts = typeof ts; export function isSvelteFilePath(filePath: string) { @@ -225,11 +226,20 @@ export function findIdentifier(ts: _ts, node: ts.Node): ts.Identifier | undefine } } -export function hasNodeModule(compilerOptions: ts.CompilerOptions, module: string) { +export function getProjectDirectory(project: ts.server.Project) { + const compilerOptions = project.getCompilerOptions(); + + if (typeof compilerOptions.configFilePath === 'string') { + return dirname(compilerOptions.configFilePath); + } + + const packageJsonPath = join(project.getCurrentDirectory(), 'package.json'); + return project.fileExists(packageJsonPath) ? project.getCurrentDirectory() : undefined; +} + +export function hasNodeModule(startPath: string, module: string) { try { - const hasModule = - typeof compilerOptions.configFilePath !== 'string' || - require.resolve(module, { paths: [compilerOptions.configFilePath] }); + const hasModule = require.resolve(module, { paths: [startPath] }); return hasModule; } catch (e) { // If require.resolve fails, we end up here, which can be either because the package is not found, @@ -237,3 +247,35 @@ export function hasNodeModule(compilerOptions: ts.CompilerOptions, module: strin return (e as any)?.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED'; } } + +export function isSvelteProject(project: ts.server.Project) { + const projectDirectory = getProjectDirectory(project); + if (projectDirectory) { + return hasNodeModule(projectDirectory, 'svelte'); + } + + const packageJsons = project + .readDirectory( + project.getCurrentDirectory(), + ['.json'], + ['node_modules', 'dist', 'build'], + ['**/package.json'], + // assuming structure like packages/projectName + 3 + ) + // in case some other plugin patched readDirectory in a weird way + .filter((file) => file.endsWith('package.json') && !hasConfigInConjunction(file, project)); + + return packageJsons.some((packageJsonPath) => + hasNodeModule(dirname(packageJsonPath), 'svelte') + ); +} + +function hasConfigInConjunction(packageJsonPath: string, project: ts.server.Project) { + const dir = dirname(packageJsonPath); + + return ( + project.fileExists(join(dir, 'tsconfig.json')) || + project.fileExists(join(dir, 'jsconfig.json')) + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1b05e2d6..eb4151cbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -320,6 +320,9 @@ importers: '@types/node': specifier: ^16.0.0 version: 16.18.32 + svelte: + specifier: ^3.57.0 + version: 3.57.0 typescript: specifier: ^5.4.5 version: 5.4.5