1+ import * as ts from "../../_namespaces/ts" ;
2+ import { defer } from "../../_namespaces/Utils" ;
3+ import {
4+ baselineTsserverLogs ,
5+ closeFilesForSession ,
6+ createLoggerWithInMemoryLogs ,
7+ createSession ,
8+ openFilesForSession ,
9+ } from "../helpers/tsserver" ;
10+ import { createServerHost , libFile } from "../helpers/virtualFileSystemWithWatch" ;
11+
12+ describe ( "unittests:: tsserver:: pluginsAsync:: async loaded plugins" , ( ) => {
13+ function setup ( globalPlugins : string [ ] ) {
14+ const host = createServerHost ( [ libFile ] ) ;
15+ const session = createSession ( host , { canUseEvents : true , globalPlugins, logger : createLoggerWithInMemoryLogs ( host ) } ) ;
16+ return { host, session } ;
17+ }
18+
19+ it ( "plugins are not loaded immediately" , async ( ) => {
20+ const { host, session } = setup ( [ "plugin-a" ] ) ;
21+ let pluginModuleInstantiated = false ;
22+ let pluginInvoked = false ;
23+ host . importPlugin = async ( _root : string , _moduleName : string ) : Promise < ts . server . ModuleImportResult > => {
24+ await Promise . resolve ( ) ; // simulate at least a single turn delay
25+ pluginModuleInstantiated = true ;
26+ return {
27+ module : ( ( ) => {
28+ pluginInvoked = true ;
29+ return { create : info => info . languageService } ;
30+ } ) as ts . server . PluginModuleFactory ,
31+ error : undefined
32+ } ;
33+ } ;
34+
35+ openFilesForSession ( [ { file : "^memfs:/foo.ts" , content : "" } ] , session ) ;
36+ const projectService = session . getProjectService ( ) ;
37+
38+ session . logger . log ( `This should be false because 'executeCommand' should have already triggered plugin enablement asynchronously and there are no plugin enablements currently being processed` ) ;
39+ session . logger . log ( `hasNewPluginEnablementRequests:: ${ projectService . hasNewPluginEnablementRequests ( ) } ` ) ;
40+
41+ session . logger . log ( `Should be true because async imports have already been triggered in the background` ) ;
42+ session . logger . log ( `hasPendingPluginEnablements:: ${ projectService . hasPendingPluginEnablements ( ) } ` ) ;
43+
44+ session . logger . log ( `Should be false because resolution of async imports happens in a later turn` ) ;
45+ session . logger . log ( `pluginModuleInstantiated:: ${ pluginModuleInstantiated } ` ) ;
46+
47+ await projectService . waitForPendingPlugins ( ) ;
48+
49+ session . logger . log ( `at this point all plugin modules should have been instantiated and all plugins should have been invoked` ) ;
50+ session . logger . log ( `pluginModuleInstantiated:: ${ pluginModuleInstantiated } ` ) ;
51+ session . logger . log ( `pluginInvoked:: ${ pluginInvoked } ` ) ;
52+
53+ baselineTsserverLogs ( "pluginsAsync" , "plugins are not loaded immediately" , session ) ;
54+ } ) ;
55+
56+ it ( "plugins evaluation in correct order even if imports resolve out of order" , async ( ) => {
57+ const { host, session } = setup ( [ "plugin-a" , "plugin-b" ] ) ;
58+ const pluginADeferred = defer ( ) ;
59+ const pluginBDeferred = defer ( ) ;
60+ host . importPlugin = async ( _root : string , moduleName : string ) : Promise < ts . server . ModuleImportResult > => {
61+ session . logger . log ( `request import ${ moduleName } ` ) ;
62+ const promise = moduleName === "plugin-a" ? pluginADeferred . promise : pluginBDeferred . promise ;
63+ await promise ;
64+ session . logger . log ( `fulfill import ${ moduleName } ` ) ;
65+ return {
66+ module : ( ( ) => {
67+ session . logger . log ( `invoke plugin ${ moduleName } ` ) ;
68+ return { create : info => info . languageService } ;
69+ } ) as ts . server . PluginModuleFactory ,
70+ error : undefined
71+ } ;
72+ } ;
73+
74+ openFilesForSession ( [ { file : "^memfs:/foo.ts" , content : "" } ] , session ) ;
75+ const projectService = session . getProjectService ( ) ;
76+
77+ // wait a turn
78+ await Promise . resolve ( ) ;
79+
80+ // resolve imports out of order
81+ pluginBDeferred . resolve ( ) ;
82+ pluginADeferred . resolve ( ) ;
83+
84+ // wait for load to complete
85+ await projectService . waitForPendingPlugins ( ) ;
86+
87+ baselineTsserverLogs ( "pluginsAsync" , "plugins evaluation in correct order even if imports resolve out of order" , session ) ;
88+ } ) ;
89+
90+ it ( "sends projectsUpdatedInBackground event" , async ( ) => {
91+ const { host, session } = setup ( [ "plugin-a" ] ) ;
92+ host . importPlugin = async ( _root : string , _moduleName : string ) : Promise < ts . server . ModuleImportResult > => {
93+ await Promise . resolve ( ) ; // simulate at least a single turn delay
94+ return {
95+ module : ( ( ) => ( { create : info => info . languageService } ) ) as ts . server . PluginModuleFactory ,
96+ error : undefined
97+ } ;
98+ } ;
99+
100+ openFilesForSession ( [ { file : "^memfs:/foo.ts" , content : "" } ] , session ) ;
101+ const projectService = session . getProjectService ( ) ;
102+
103+
104+ await projectService . waitForPendingPlugins ( ) ;
105+
106+ baselineTsserverLogs ( "pluginsAsync" , "sends projectsUpdatedInBackground event" , session ) ;
107+ } ) ;
108+
109+ it ( "adds external files" , async ( ) => {
110+ const { host, session } = setup ( [ "plugin-a" ] ) ;
111+ const pluginAShouldLoad = defer ( ) ;
112+ host . importPlugin = async ( _root : string , _moduleName : string ) : Promise < ts . server . ModuleImportResult > => {
113+ // wait until the initial external files are requested from the project service.
114+ await pluginAShouldLoad . promise ;
115+
116+ return {
117+ module : ( ( ) => ( {
118+ create : info => info . languageService ,
119+ getExternalFiles : ( ) => [ "external.txt" ] ,
120+ } ) ) as ts . server . PluginModuleFactory ,
121+ error : undefined
122+ } ;
123+ } ;
124+
125+ openFilesForSession ( [ { file : "^memfs:/foo.ts" , content : "" } ] , session ) ;
126+ const projectService = session . getProjectService ( ) ;
127+
128+ const project = projectService . inferredProjects [ 0 ] ;
129+
130+ session . logger . log ( `External files before plugin is loaded: ${ project . getExternalFiles ( ) . join ( "," ) } ` ) ;
131+ // we've ready the initial set of external files, allow the plugin to continue loading.
132+ pluginAShouldLoad . resolve ( ) ;
133+
134+ // wait for plugins
135+ await projectService . waitForPendingPlugins ( ) ;
136+
137+ host . runQueuedTimeoutCallbacks ( ) ;
138+
139+ session . logger . log ( `External files before plugin after plugin is loaded: ${ project . getExternalFiles ( ) . join ( "," ) } ` ) ;
140+ baselineTsserverLogs ( "pluginsAsync" , "adds external files" , session ) ;
141+ } ) ;
142+
143+ it ( "project is closed before plugins are loaded" , async ( ) => {
144+ const { host, session } = setup ( [ "plugin-a" ] ) ;
145+ const pluginALoaded = defer ( ) ;
146+ const projectClosed = defer ( ) ;
147+ host . importPlugin = async ( _root : string , _moduleName : string ) : Promise < ts . server . ModuleImportResult > => {
148+ // mark that the plugin has started loading
149+ pluginALoaded . resolve ( ) ;
150+
151+ // wait until after a project close has been requested to continue
152+ await projectClosed . promise ;
153+ return {
154+ module : ( ( ) => ( { create : info => info . languageService } ) ) as ts . server . PluginModuleFactory ,
155+ error : undefined
156+ } ;
157+ } ;
158+
159+ openFilesForSession ( [ { file : "^memfs:/foo.ts" , content : "" } ] , session ) ;
160+ const projectService = session . getProjectService ( ) ;
161+
162+
163+ // wait for the plugin to start loading
164+ await pluginALoaded . promise ;
165+
166+ // close the project
167+ closeFilesForSession ( [ "^memfs:/foo.ts" ] , session ) ;
168+
169+ // continue loading the plugin
170+ projectClosed . resolve ( ) ;
171+
172+ await projectService . waitForPendingPlugins ( ) ;
173+
174+ // the project was closed before plugins were ready. no project update should have been requested
175+ baselineTsserverLogs ( "pluginsAsync" , "project is closed before plugins are loaded" , session ) ;
176+ } ) ;
177+ } ) ;
0 commit comments