From db33c9312de34d63a6b2bfc3e034643725de9b82 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Thu, 7 Mar 2024 12:07:11 +0800 Subject: [PATCH 1/6] detect svelte project when no tsconfig/jsconfig --- packages/typescript-plugin/package.json | 3 ++- packages/typescript-plugin/src/index.ts | 15 ++++++++++----- .../src/language-service/sveltekit.ts | 5 +++-- packages/typescript-plugin/src/utils.ts | 18 ++++++++++++++---- pnpm-lock.yaml | 3 +++ 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index e47708ffa..301ce910c 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.3.2" + "typescript": "^5.3.2", + "svelte": "*" }, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.14", diff --git a/packages/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts index 3c85949ae..4b7dda684 100644 --- a/packages/typescript-plugin/src/index.ts +++ b/packages/typescript-plugin/src/index.ts @@ -6,7 +6,7 @@ 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, getProjectDirectory, hasNodeModule } from './utils'; function init(modules: { typescript: typeof ts }): ts.server.PluginModule { const configManager = new ConfigManager(); @@ -16,7 +16,7 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { const logger = new Logger(info.project.projectService.logger); if ( !(info.config as Configuration)?.assumeIsSvelteProject && - !isSvelteProject(info.project.getCompilerOptions()) + !isSvelteProject(info.project) ) { logger.log('Detected that this is not a Svelte project, abort patching TypeScript'); return info.languageService; @@ -218,9 +218,14 @@ 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 isSvelteProject(project: ts.server.Project) { + const projectDirectory = getProjectDirectory(project); + if (projectDirectory) { + return hasNodeModule(projectDirectory, 'svelte'); + } + + // getScriptFileNames is for files open in the editor in inferred projects + return project.getScriptInfos().some((info) => info.fileName.endsWith('.svelte')); } 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/utils.ts b/packages/typescript-plugin/src/utils.ts index 3cdefee9b..47c4451db 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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 612bda377..481c6c108 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: '*' + version: 3.57.0 typescript: specifier: ^5.3.2 version: 5.3.2 From 61ec426a8af6d3ce733ddadc08c63e24c92e3883 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Thu, 7 Mar 2024 12:14:22 +0800 Subject: [PATCH 2/6] delay enable until _typescript.configurePlugin request --- packages/typescript-plugin/src/config-manager.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/typescript-plugin/src/config-manager.ts b/packages/typescript-plugin/src/config-manager.ts index 82f046598..1fd8d2c34 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; @@ -25,9 +26,12 @@ export class ConfigManager { } 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); } From 071c8ad5b085fa15079e659ce5e602c4eb583d54 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 8 Mar 2024 10:16:08 +0800 Subject: [PATCH 3/6] cleanup listener --- .../typescript-plugin/src/config-manager.ts | 4 ++++ packages/typescript-plugin/src/index.ts | 19 ++++++++++++------- .../typescript-plugin/src/module-loader.ts | 14 +++++++++++--- .../src/project-svelte-files.ts | 8 +++++--- packages/typescript-plugin/src/svelte-sys.ts | 2 +- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/packages/typescript-plugin/src/config-manager.ts b/packages/typescript-plugin/src/config-manager.ts index 1fd8d2c34..4efd9a019 100644 --- a/packages/typescript-plugin/src/config-manager.ts +++ b/packages/typescript-plugin/src/config-manager.ts @@ -20,6 +20,10 @@ 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; diff --git a/packages/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts index 4b7dda684..efb706289 100644 --- a/packages/typescript-plugin/src/index.ts +++ b/packages/typescript-plugin/src/index.ts @@ -126,7 +126,7 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { ) : undefined; - patchModuleLoader( + const moduleLoaderDisposable = patchModuleLoader( logger, snapshotManager, modules.typescript, @@ -135,7 +135,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 +147,8 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { if (projectSvelteFilesManager) { info.project.updateGraph(); } - }); + }; + configManager.onConfigurationChanged(updateProjectWhenConfigChanges); return decorateLanguageService( info.languageService, @@ -156,12 +157,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 (!isSvelteProject(project) || !configManager.getConfig().enable) { return []; } @@ -224,8 +229,8 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { return hasNodeModule(projectDirectory, 'svelte'); } - // getScriptFileNames is for files open in the editor in inferred projects - return project.getScriptInfos().some((info) => info.fileName.endsWith('.svelte')); + // TODO: check for svelte files in the project + return false; } function onConfigurationChanged(config: Configuration) { diff --git a/packages/typescript-plugin/src/module-loader.ts b/packages/typescript-plugin/src/module-loader.ts index 6da7c5cc9..e15ce30cb 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..8e8de370d 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(); @@ -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'; From ac72961c9b49878300e80578469e42e41a91789f Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 8 Mar 2024 12:00:59 +0800 Subject: [PATCH 4/6] wip --- packages/typescript-plugin/src/index.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts index efb706289..1b74aa492 100644 --- a/packages/typescript-plugin/src/index.ts +++ b/packages/typescript-plugin/src/index.ts @@ -229,8 +229,16 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { return hasNodeModule(projectDirectory, 'svelte'); } - // TODO: check for svelte files in the project - return false; + return project.readDirectory( + project.getCurrentDirectory(), + ['.svelte'], + ['node_modules', 'dist'], + undefined, + + // assuming structure like + // packages/app/src/lib + 6 + ).length > 0; } function onConfigurationChanged(config: Configuration) { From 3ee0077ff276da821b12f3509f675498ad74725f Mon Sep 17 00:00:00 2001 From: Lyu Jason Date: Thu, 14 Mar 2024 20:56:16 +0800 Subject: [PATCH 5/6] check nested package.json for svelte project --- packages/typescript-plugin/src/index.ts | 28 +++++++--------- .../typescript-plugin/src/module-loader.ts | 6 ++-- .../src/project-svelte-files.ts | 2 +- packages/typescript-plugin/src/utils.ts | 32 +++++++++++++++++++ 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/packages/typescript-plugin/src/index.ts b/packages/typescript-plugin/src/index.ts index 1b74aa492..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, getProjectDirectory, 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) + !isSvelteProjectWithCache(info.project) ) { logger.log('Detected that this is not a Svelte project, abort patching TypeScript'); return info.languageService; @@ -166,7 +167,7 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { } function getExternalFiles(project: ts.server.Project) { - if (!isSvelteProject(project) || !configManager.getConfig().enable) { + if (!isSvelteProjectWithCache(project) || !configManager.getConfig().enable) { return []; } @@ -223,22 +224,15 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule { return svelteTsxFiles; } - function isSvelteProject(project: ts.server.Project) { - const projectDirectory = getProjectDirectory(project); - if (projectDirectory) { - return hasNodeModule(projectDirectory, 'svelte'); + function isSvelteProjectWithCache(project: ts.server.Project) { + const cached = isSvelteProjectCache.get(project.getProjectName()); + if (cached !== undefined) { + return cached; } - return project.readDirectory( - project.getCurrentDirectory(), - ['.svelte'], - ['node_modules', 'dist'], - undefined, - - // assuming structure like - // packages/app/src/lib - 6 - ).length > 0; + const result = !!isSvelteProject(project); + isSvelteProjectCache.set(project.getProjectName(), result); + return result; } function onConfigurationChanged(config: Configuration) { diff --git a/packages/typescript-plugin/src/module-loader.ts b/packages/typescript-plugin/src/module-loader.ts index e15ce30cb..cc2003cba 100644 --- a/packages/typescript-plugin/src/module-loader.ts +++ b/packages/typescript-plugin/src/module-loader.ts @@ -122,15 +122,15 @@ export function patchModuleLoader( 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 8e8de370d..e9cea747e 100644 --- a/packages/typescript-plugin/src/project-svelte-files.ts +++ b/packages/typescript-plugin/src/project-svelte-files.ts @@ -170,7 +170,7 @@ export class ProjectSvelteFilesManager { this.setupWatchers(); this.updateProjectSvelteFiles(); } - } + }; private removeFileFromProject(file: string, exists = true) { const info = this.project.getScriptInfo(file); diff --git a/packages/typescript-plugin/src/utils.ts b/packages/typescript-plugin/src/utils.ts index 47c4451db..92b0a58c6 100644 --- a/packages/typescript-plugin/src/utils.ts +++ b/packages/typescript-plugin/src/utils.ts @@ -247,3 +247,35 @@ export function hasNodeModule(startPath: string, module: string) { 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')) + ); +} From cbec2c0097bcb0b5f95c98a290b9ef9678860845 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Mon, 18 Mar 2024 12:42:59 +0800 Subject: [PATCH 6/6] more specific version --- packages/typescript-plugin/package.json | 2 +- pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typescript-plugin/package.json b/packages/typescript-plugin/package.json index 301ce910c..c03d9aaf6 100644 --- a/packages/typescript-plugin/package.json +++ b/packages/typescript-plugin/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@types/node": "^16.0.0", "typescript": "^5.3.2", - "svelte": "*" + "svelte": "^3.57.0" }, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc513a756..e63865679 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -321,7 +321,7 @@ importers: specifier: ^16.0.0 version: 16.18.32 svelte: - specifier: '*' + specifier: ^3.57.0 version: 3.57.0 typescript: specifier: ^5.3.2