Skip to content

Commit

Permalink
fix: volar ts plugin compatibility (#2317)
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
jasonlyu123 authored Apr 25, 2024
1 parent d9e8948 commit a8e45c6
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 26 deletions.
3 changes: 2 additions & 1 deletion packages/typescript-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 9 additions & 1 deletion packages/typescript-plugin/src/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down
32 changes: 22 additions & 10 deletions packages/typescript-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, boolean>();

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;
Expand Down Expand Up @@ -126,7 +127,7 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
)
: undefined;

patchModuleLoader(
const moduleLoaderDisposable = patchModuleLoader(
logger,
snapshotManager,
modules.typescript,
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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 [];
}

Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions packages/typescript-plugin/src/language-service/sveltekit.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
Expand Down
14 changes: 11 additions & 3 deletions packages/typescript-plugin/src/module-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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[],
Expand Down
10 changes: 6 additions & 4 deletions packages/typescript-plugin/src/project-svelte-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -162,15 +162,15 @@ export class ProjectSvelteFilesManager {
.map(this.typescript.server.toNormalizedPath);
}

private onConfigChanged(config: Configuration) {
private onConfigChanged = (config: Configuration) => {
this.disposeWatchers();
this.clearProjectFile();

if (config.enable) {
this.setupWatchers();
this.updateProjectSvelteFiles();
}
}
};

private removeFileFromProject(file: string, exists = true) {
const info = this.project.getScriptInfo(file);
Expand Down Expand Up @@ -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());
}
}
2 changes: 1 addition & 1 deletion packages/typescript-plugin/src/svelte-sys.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
50 changes: 46 additions & 4 deletions packages/typescript-plugin/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -225,15 +226,56 @@ 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,
// or (in case of things like SvelteKit) the package is found but the package.json is not exported.
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'))
);
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a8e45c6

Please sign in to comment.