Skip to content

Commit 9c92206

Browse files
authored
(feat) Support svelte project files in typescript plugin (#1407)
#1195
1 parent d6327c4 commit 9c92206

File tree

4 files changed

+244
-16
lines changed

4 files changed

+244
-16
lines changed

packages/typescript-plugin/src/index.ts

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { patchModuleLoader } from './module-loader';
55
import { SvelteSnapshotManager } from './svelte-snapshots';
66
import type ts from 'typescript/lib/tsserverlibrary';
77
import { ConfigManager, Configuration } from './config-manager';
8+
import { ProjectSvelteFilesManager } from './project-svelte-files';
9+
import { getConfigPathForProject } from './utils';
810

9-
function init(modules: { typescript: typeof ts }) {
11+
function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
1012
const configManager = new ConfigManager();
1113

1214
function create(info: ts.server.PluginCreateInfo) {
@@ -17,7 +19,12 @@ function init(modules: { typescript: typeof ts }) {
1719
}
1820

1921
if (isPatched(info.languageService)) {
20-
logger.log('Already patched');
22+
logger.log('Already patched. Checking tsconfig updates.');
23+
24+
ProjectSvelteFilesManager.getInstance(
25+
info.project.getProjectName()
26+
)?.updateProjectConfig(info.languageServiceHost);
27+
2128
return info.languageService;
2229
}
2330

@@ -29,12 +36,15 @@ function init(modules: { typescript: typeof ts }) {
2936
logger.log(info.config);
3037
}
3138

32-
// If someone knows a better/more performant way to get svelteOptions,
33-
// please tell us :)
34-
const svelteOptions = info.languageServiceHost.getParsedCommandLine?.(
35-
(info.project.getCompilerOptions() as any).configFilePath
36-
)?.raw?.svelteOptions || { namespace: 'svelteHTML' };
39+
// This call the ConfiguredProject.getParsedCommandLine
40+
// where it'll try to load the cached version of the parsedCommandLine
41+
const parsedCommandLine = info.languageServiceHost.getParsedCommandLine?.(
42+
getConfigPathForProject(info.project)
43+
);
44+
45+
const svelteOptions = parsedCommandLine?.raw?.svelteOptions || { namespace: 'svelteHTML' };
3746
logger.log('svelteOptions:', svelteOptions);
47+
logger.debug(parsedCommandLine?.wildcardDirectories);
3848

3949
const snapshotManager = new SvelteSnapshotManager(
4050
modules.typescript,
@@ -44,6 +54,17 @@ function init(modules: { typescript: typeof ts }) {
4454
configManager
4555
);
4656

57+
const projectSvelteFilesManager = parsedCommandLine
58+
? new ProjectSvelteFilesManager(
59+
modules.typescript,
60+
info.project,
61+
info.serverHost,
62+
snapshotManager,
63+
parsedCommandLine,
64+
configManager
65+
)
66+
: undefined;
67+
4768
patchModuleLoader(
4869
logger,
4970
snapshotManager,
@@ -57,18 +78,24 @@ function init(modules: { typescript: typeof ts }) {
5778
// enabling/disabling the plugin means TS has to recompute stuff
5879
info.languageService.cleanupSemanticCache();
5980
info.project.markAsDirty();
81+
82+
// updateGraph checks for new root files
83+
// if there's no tsconfig there isn't root files to check
84+
if (projectSvelteFilesManager) {
85+
info.project.updateGraph();
86+
}
6087
});
6188

62-
return decorateLanguageService(
63-
info.languageService,
64-
snapshotManager,
65-
logger,
66-
configManager
89+
return decorateLanguageServiceDispose(
90+
decorateLanguageService(info.languageService, snapshotManager, logger, configManager),
91+
projectSvelteFilesManager ?? {
92+
dispose() {}
93+
}
6794
);
6895
}
6996

70-
function getExternalFiles(project: ts.server.ConfiguredProject) {
71-
if (!isSvelteProject(project.getCompilerOptions())) {
97+
function getExternalFiles(project: ts.server.Project) {
98+
if (!isSvelteProject(project.getCompilerOptions()) || !configManager.getConfig().enable) {
7299
return [];
73100
}
74101

@@ -79,7 +106,11 @@ function init(modules: { typescript: typeof ts }) {
79106
'./svelte-jsx.d.ts',
80107
'./svelte-native-jsx.d.ts'
81108
].map((f) => modules.typescript.sys.resolvePath(resolve(svelteTsPath, f)));
82-
return svelteTsxFiles;
109+
110+
// let ts know project svelte files to do its optimization
111+
return svelteTsxFiles.concat(
112+
ProjectSvelteFilesManager.getInstance(project.getProjectName())?.getFiles() ?? []
113+
);
83114
}
84115

85116
function isSvelteProject(compilerOptions: ts.CompilerOptions) {
@@ -99,6 +130,20 @@ function init(modules: { typescript: typeof ts }) {
99130
configManager.updateConfigFromPluginConfig(config);
100131
}
101132

133+
function decorateLanguageServiceDispose(
134+
languageService: ts.LanguageService,
135+
disposable: { dispose(): void }
136+
) {
137+
const dispose = languageService.dispose;
138+
139+
languageService.dispose = () => {
140+
disposable.dispose();
141+
dispose();
142+
};
143+
144+
return languageService;
145+
}
146+
102147
return { create, getExternalFiles, onConfigurationChanged };
103148
}
104149

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ function createProxyHandler(configManager: ConfigManager): ProxyHandler<ts.Langu
5454
return true;
5555
}
5656

57-
if (!configManager.getConfig().enable) {
57+
if (!configManager.getConfig().enable || p === 'dispose') {
5858
return target[p as keyof ts.LanguageService];
5959
}
6060

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import type ts from 'typescript/lib/tsserverlibrary';
2+
import { ConfigManager, Configuration } from './config-manager';
3+
import { SvelteSnapshotManager } from './svelte-snapshots';
4+
import { getConfigPathForProject, isSvelteFilePath } from './utils';
5+
6+
export interface TsFilesSpec {
7+
include?: readonly string[];
8+
exclude?: readonly string[];
9+
}
10+
11+
export class ProjectSvelteFilesManager {
12+
private files = new Set<string>();
13+
private directoryWatchers = new Set<ts.FileWatcher>();
14+
15+
private static instances = new Map<string, ProjectSvelteFilesManager>();
16+
17+
static getInstance(projectName: string) {
18+
return this.instances.get(projectName);
19+
}
20+
21+
constructor(
22+
private readonly typescript: typeof ts,
23+
private readonly project: ts.server.Project,
24+
private readonly serverHost: ts.server.ServerHost,
25+
private readonly snapshotManager: SvelteSnapshotManager,
26+
private parsedCommandLine: ts.ParsedCommandLine,
27+
configManager: ConfigManager
28+
) {
29+
if (configManager.getConfig().enable) {
30+
this.setupWatchers();
31+
this.updateProjectSvelteFiles();
32+
}
33+
34+
configManager.onConfigurationChanged(this.onConfigChanged.bind(this));
35+
ProjectSvelteFilesManager.instances.set(project.getProjectName(), this);
36+
}
37+
38+
updateProjectConfig(serviceHost: ts.LanguageServiceHost) {
39+
const parsedCommandLine = serviceHost.getParsedCommandLine?.(
40+
getConfigPathForProject(this.project)
41+
);
42+
43+
if (!parsedCommandLine) {
44+
return;
45+
}
46+
47+
this.disposeWatchersAndFiles();
48+
this.parsedCommandLine = parsedCommandLine;
49+
this.setupWatchers();
50+
this.updateProjectSvelteFiles();
51+
}
52+
53+
getFiles() {
54+
return Array.from(this.files);
55+
}
56+
57+
/**
58+
* Create directory watcher for include and exclude
59+
* The watcher in tsserver doesn't support svelte file
60+
* It won't add new created svelte file to root
61+
*/
62+
private setupWatchers() {
63+
for (const directory in this.parsedCommandLine.wildcardDirectories) {
64+
if (
65+
!Object.prototype.hasOwnProperty.call(
66+
this.parsedCommandLine.wildcardDirectories,
67+
directory
68+
)
69+
) {
70+
continue;
71+
}
72+
73+
const watchDirectoryFlags = this.parsedCommandLine.wildcardDirectories[directory];
74+
const watcher = this.serverHost.watchDirectory(
75+
directory,
76+
this.watcherCallback.bind(this),
77+
watchDirectoryFlags === this.typescript.WatchDirectoryFlags.Recursive,
78+
this.parsedCommandLine.watchOptions
79+
);
80+
81+
this.directoryWatchers.add(watcher);
82+
}
83+
}
84+
85+
private watcherCallback(fileName: string) {
86+
if (!isSvelteFilePath(fileName)) {
87+
return;
88+
}
89+
90+
// We can't just add the file to the project directly, because
91+
// - the casing of fileName is different
92+
// - we don't know whether the file was added or deleted
93+
this.updateProjectSvelteFiles();
94+
}
95+
96+
private updateProjectSvelteFiles() {
97+
const fileNamesAfter = this.readProjectSvelteFilesFromFs();
98+
const removedFiles = new Set(...this.files);
99+
const newFiles = fileNamesAfter.filter((fileName) => {
100+
const has = this.files.has(fileName);
101+
if (has) {
102+
removedFiles.delete(fileName);
103+
}
104+
return !has;
105+
});
106+
107+
for (const newFile of newFiles) {
108+
this.addFileToProject(newFile);
109+
this.files.add(newFile);
110+
}
111+
for (const removedFile of removedFiles) {
112+
this.removeFileFromProject(removedFile, false);
113+
this.files.delete(removedFile);
114+
}
115+
}
116+
117+
private addFileToProject(newFile: string) {
118+
this.snapshotManager.create(newFile);
119+
const snapshot = this.project.projectService.getScriptInfo(newFile);
120+
121+
if (snapshot) {
122+
this.project.addRoot(snapshot);
123+
}
124+
}
125+
126+
private readProjectSvelteFilesFromFs() {
127+
const fileSpec: TsFilesSpec = this.parsedCommandLine.raw;
128+
const { include, exclude } = fileSpec;
129+
130+
if (include?.length === 0) {
131+
return [];
132+
}
133+
134+
return this.typescript.sys
135+
.readDirectory(
136+
this.project.getCurrentDirectory() || process.cwd(),
137+
['.svelte'],
138+
exclude,
139+
include
140+
)
141+
.map(this.typescript.server.toNormalizedPath);
142+
}
143+
144+
private onConfigChanged(config: Configuration) {
145+
this.disposeWatchersAndFiles();
146+
147+
if (config.enable) {
148+
this.setupWatchers();
149+
this.updateProjectSvelteFiles();
150+
}
151+
}
152+
153+
private removeFileFromProject(file: string, exists = true) {
154+
const info = this.project.getScriptInfo(file);
155+
156+
if (info) {
157+
this.project.removeFile(info, exists, true);
158+
}
159+
}
160+
161+
private disposeWatchersAndFiles() {
162+
this.directoryWatchers.forEach((watcher) => watcher.close());
163+
this.directoryWatchers.clear();
164+
165+
this.files.forEach((file) => this.removeFileFromProject(file));
166+
this.files.clear();
167+
}
168+
169+
dispose() {
170+
this.disposeWatchersAndFiles();
171+
172+
ProjectSvelteFilesManager.instances.delete(this.project.getProjectName());
173+
}
174+
}

packages/typescript-plugin/src/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type ts from 'typescript/lib/tsserverlibrary';
2+
13
export function isSvelteFilePath(filePath: string) {
24
return filePath.endsWith('.svelte');
35
}
@@ -64,3 +66,10 @@ export function replaceDeep<T extends Record<string, any>>(
6466
return _obj;
6567
}
6668
}
69+
70+
export function getConfigPathForProject(project: ts.server.Project) {
71+
return (
72+
(project as ts.server.ConfiguredProject).canonicalConfigFilePath ??
73+
(project.getCompilerOptions() as any).configFilePath
74+
);
75+
}

0 commit comments

Comments
 (0)