Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: volar ts plugin compatibility #2317

Merged
merged 9 commits into from
Apr 25, 2024
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.3.2"
"typescript": "^5.3.2",
"svelte": "*"
jasonlyu123 marked this conversation as resolved.
Show resolved Hide resolved
},
"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.

Loading