Skip to content

Commit a8e45c6

Browse files
authored
fix: volar ts plugin compatibility (#2317)
#2307 Improve the svelte project check for InferredProject. Start from the current directory and go down 2 levels in the directory tree to search for package.json. Then we use the directories with package.json to search for svelte modules. If your ts-plugin is no longer enabled, you can create a jsconfig.json where your package.json is. This alone should resolve most problems since the plugin won't be applied at all. Disable the plugin in the config level until we receive the _typescript.configurePlugin request. This should decrease the chance of the project update because of the config toggle.
1 parent d9e8948 commit a8e45c6

File tree

9 files changed

+103
-26
lines changed

9 files changed

+103
-26
lines changed

packages/typescript-plugin/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"license": "MIT",
2020
"devDependencies": {
2121
"@types/node": "^16.0.0",
22-
"typescript": "^5.4.5"
22+
"typescript": "^5.4.5",
23+
"svelte": "^3.57.0"
2324
},
2425
"dependencies": {
2526
"@jridgewell/sourcemap-codec": "^1.4.14",

packages/typescript-plugin/src/config-manager.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { EventEmitter } from 'events';
33
const configurationEventName = 'configuration-changed';
44

55
export interface Configuration {
6+
global?: boolean;
67
enable: boolean;
78
/** Skip the Svelte detection and assume this is a Svelte project */
89
assumeIsSvelteProject: boolean;
@@ -19,15 +20,22 @@ export class ConfigManager {
1920
this.emitter.on(configurationEventName, listener);
2021
}
2122

23+
removeConfigurationChangeListener(listener: (config: Configuration) => void) {
24+
this.emitter.off(configurationEventName, listener);
25+
}
26+
2227
isConfigChanged(config: Configuration) {
2328
// right now we only care about enable
2429
return config.enable !== this.config.enable;
2530
}
2631

2732
updateConfigFromPluginConfig(config: Configuration) {
33+
const shouldWaitForConfigRequest = config.global == true;
34+
const enable = config.enable ?? !shouldWaitForConfigRequest;
2835
this.config = {
2936
...this.config,
30-
...config
37+
...config,
38+
enable
3139
};
3240
this.emitter.emit(configurationEventName, config);
3341
}

packages/typescript-plugin/src/index.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ import { SvelteSnapshotManager } from './svelte-snapshots';
66
import type ts from 'typescript/lib/tsserverlibrary';
77
import { ConfigManager, Configuration } from './config-manager';
88
import { ProjectSvelteFilesManager } from './project-svelte-files';
9-
import { getConfigPathForProject, hasNodeModule } from './utils';
9+
import { getConfigPathForProject, isSvelteProject } from './utils';
1010

1111
function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
1212
const configManager = new ConfigManager();
1313
let resolvedSvelteTsxFiles: string[] | undefined;
14+
const isSvelteProjectCache = new Map<string, boolean>();
1415

1516
function create(info: ts.server.PluginCreateInfo) {
1617
const logger = new Logger(info.project.projectService.logger);
1718
if (
1819
!(info.config as Configuration)?.assumeIsSvelteProject &&
19-
!isSvelteProject(info.project.getCompilerOptions())
20+
!isSvelteProjectWithCache(info.project)
2021
) {
2122
logger.log('Detected that this is not a Svelte project, abort patching TypeScript');
2223
return info.languageService;
@@ -126,7 +127,7 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
126127
)
127128
: undefined;
128129

129-
patchModuleLoader(
130+
const moduleLoaderDisposable = patchModuleLoader(
130131
logger,
131132
snapshotManager,
132133
modules.typescript,
@@ -135,7 +136,7 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
135136
configManager
136137
);
137138

138-
configManager.onConfigurationChanged(() => {
139+
const updateProjectWhenConfigChanges = () => {
139140
// enabling/disabling the plugin means TS has to recompute stuff
140141
// don't clear semantic cache here
141142
// 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 {
147148
if (projectSvelteFilesManager) {
148149
info.project.updateGraph();
149150
}
150-
});
151+
};
152+
configManager.onConfigurationChanged(updateProjectWhenConfigChanges);
151153

152154
return decorateLanguageService(
153155
info.languageService,
@@ -156,12 +158,16 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
156158
configManager,
157159
info,
158160
modules.typescript,
159-
() => projectSvelteFilesManager?.dispose()
161+
() => {
162+
projectSvelteFilesManager?.dispose();
163+
configManager.removeConfigurationChangeListener(updateProjectWhenConfigChanges);
164+
moduleLoaderDisposable.dispose();
165+
}
160166
);
161167
}
162168

163169
function getExternalFiles(project: ts.server.Project) {
164-
if (!isSvelteProject(project.getCompilerOptions()) || !configManager.getConfig().enable) {
170+
if (!isSvelteProjectWithCache(project) || !configManager.getConfig().enable) {
165171
return [];
166172
}
167173

@@ -218,9 +224,15 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
218224
return svelteTsxFiles;
219225
}
220226

221-
function isSvelteProject(compilerOptions: ts.CompilerOptions) {
222-
// Add more checks like "no Svelte file found" or "no config file found"?
223-
return hasNodeModule(compilerOptions, 'svelte');
227+
function isSvelteProjectWithCache(project: ts.server.Project) {
228+
const cached = isSvelteProjectCache.get(project.getProjectName());
229+
if (cached !== undefined) {
230+
return cached;
231+
}
232+
233+
const result = !!isSvelteProject(project);
234+
isSvelteProjectCache.set(project.getProjectName(), result);
235+
return result;
224236
}
225237

226238
function onConfigurationChanged(config: Configuration) {

packages/typescript-plugin/src/language-service/sveltekit.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type ts from 'typescript/lib/tsserverlibrary';
22
import { Logger } from '../logger';
3-
import { hasNodeModule } from '../utils';
3+
import { getProjectDirectory, hasNodeModule } from '../utils';
44
import { InternalHelpers, internalHelpers } from 'svelte2tsx';
55
type _ts = typeof ts;
66

@@ -531,7 +531,8 @@ function getProxiedLanguageService(info: ts.server.PluginCreateInfo, ts: _ts, lo
531531
return cachedProxiedLanguageService ?? undefined;
532532
}
533533

534-
if (!hasNodeModule(info.project.getCompilerOptions(), '@sveltejs/kit')) {
534+
const projectDirectory = getProjectDirectory(info.project);
535+
if (projectDirectory && !hasNodeModule(projectDirectory, '@sveltejs/kit')) {
535536
// Not a SvelteKit project, do nothing
536537
cache.set(info, null);
537538
return;

packages/typescript-plugin/src/module-loader.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export function patchModuleLoader(
101101
lsHost: ts.LanguageServiceHost,
102102
project: ts.server.Project,
103103
configManager: ConfigManager
104-
): void {
104+
): { dispose: () => void } {
105105
const svelteSys = createSvelteSys(typescript, logger);
106106
const moduleCache = new ModuleResolutionCache(project.projectService);
107107
const origResolveModuleNames = lsHost.resolveModuleNames?.bind(lsHost);
@@ -120,9 +120,17 @@ export function patchModuleLoader(
120120
return origRemoveFile(info, fileExists, detachFromProject);
121121
};
122122

123-
configManager.onConfigurationChanged(() => {
123+
const onConfigChanged = () => {
124124
moduleCache.clear();
125-
});
125+
};
126+
configManager.onConfigurationChanged(onConfigChanged);
127+
128+
return {
129+
dispose() {
130+
configManager.removeConfigurationChangeListener(onConfigChanged);
131+
moduleCache.clear();
132+
}
133+
};
126134

127135
function resolveModuleNames(
128136
moduleNames: string[],

packages/typescript-plugin/src/project-svelte-files.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@ export class ProjectSvelteFilesManager {
2626
private readonly snapshotManager: SvelteSnapshotManager,
2727
private readonly logger: Logger,
2828
private parsedCommandLine: ts.ParsedCommandLine,
29-
configManager: ConfigManager
29+
private readonly configManager: ConfigManager
3030
) {
3131
if (configManager.getConfig().enable) {
3232
this.setupWatchers();
3333
this.updateProjectSvelteFiles();
3434
}
3535

36-
configManager.onConfigurationChanged(this.onConfigChanged.bind(this));
36+
configManager.onConfigurationChanged(this.onConfigChanged);
3737
ProjectSvelteFilesManager.instances.set(project.getProjectName(), this);
3838
}
3939

@@ -162,15 +162,15 @@ export class ProjectSvelteFilesManager {
162162
.map(this.typescript.server.toNormalizedPath);
163163
}
164164

165-
private onConfigChanged(config: Configuration) {
165+
private onConfigChanged = (config: Configuration) => {
166166
this.disposeWatchers();
167167
this.clearProjectFile();
168168

169169
if (config.enable) {
170170
this.setupWatchers();
171171
this.updateProjectSvelteFiles();
172172
}
173-
}
173+
};
174174

175175
private removeFileFromProject(file: string, exists = true) {
176176
const info = this.project.getScriptInfo(file);
@@ -198,6 +198,8 @@ export class ProjectSvelteFilesManager {
198198
// - and because the project is closed, `project.removeFile` will result in an error
199199
this.projectFileToOriginalCasing.clear();
200200

201+
this.configManager.removeConfigurationChangeListener(this.onConfigChanged);
202+
201203
ProjectSvelteFilesManager.instances.delete(this.project.getProjectName());
202204
}
203205
}

packages/typescript-plugin/src/svelte-sys.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type ts from 'typescript';
1+
import type ts from 'typescript/lib/tsserverlibrary';
22
import { Logger } from './logger';
33
import { ensureRealSvelteFilePath, isVirtualSvelteFilePath, toRealSvelteFilePath } from './utils';
44

packages/typescript-plugin/src/utils.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type ts from 'typescript/lib/tsserverlibrary';
22
import { SvelteSnapshot } from './svelte-snapshots';
3+
import { dirname, join } from 'path';
34
type _ts = typeof ts;
45

56
export function isSvelteFilePath(filePath: string) {
@@ -225,15 +226,56 @@ export function findIdentifier(ts: _ts, node: ts.Node): ts.Identifier | undefine
225226
}
226227
}
227228

228-
export function hasNodeModule(compilerOptions: ts.CompilerOptions, module: string) {
229+
export function getProjectDirectory(project: ts.server.Project) {
230+
const compilerOptions = project.getCompilerOptions();
231+
232+
if (typeof compilerOptions.configFilePath === 'string') {
233+
return dirname(compilerOptions.configFilePath);
234+
}
235+
236+
const packageJsonPath = join(project.getCurrentDirectory(), 'package.json');
237+
return project.fileExists(packageJsonPath) ? project.getCurrentDirectory() : undefined;
238+
}
239+
240+
export function hasNodeModule(startPath: string, module: string) {
229241
try {
230-
const hasModule =
231-
typeof compilerOptions.configFilePath !== 'string' ||
232-
require.resolve(module, { paths: [compilerOptions.configFilePath] });
242+
const hasModule = require.resolve(module, { paths: [startPath] });
233243
return hasModule;
234244
} catch (e) {
235245
// If require.resolve fails, we end up here, which can be either because the package is not found,
236246
// or (in case of things like SvelteKit) the package is found but the package.json is not exported.
237247
return (e as any)?.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED';
238248
}
239249
}
250+
251+
export function isSvelteProject(project: ts.server.Project) {
252+
const projectDirectory = getProjectDirectory(project);
253+
if (projectDirectory) {
254+
return hasNodeModule(projectDirectory, 'svelte');
255+
}
256+
257+
const packageJsons = project
258+
.readDirectory(
259+
project.getCurrentDirectory(),
260+
['.json'],
261+
['node_modules', 'dist', 'build'],
262+
['**/package.json'],
263+
// assuming structure like packages/projectName
264+
3
265+
)
266+
// in case some other plugin patched readDirectory in a weird way
267+
.filter((file) => file.endsWith('package.json') && !hasConfigInConjunction(file, project));
268+
269+
return packageJsons.some((packageJsonPath) =>
270+
hasNodeModule(dirname(packageJsonPath), 'svelte')
271+
);
272+
}
273+
274+
function hasConfigInConjunction(packageJsonPath: string, project: ts.server.Project) {
275+
const dir = dirname(packageJsonPath);
276+
277+
return (
278+
project.fileExists(join(dir, 'tsconfig.json')) ||
279+
project.fileExists(join(dir, 'jsconfig.json'))
280+
);
281+
}

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)