-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Enable TS Server plugins on web #47377
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
Changes from all commits
e2bbb6c
53d90b6
34a83a3
51680a1
85e0fca
f40a867
186cec9
39dac60
78e016c
c760041
9bc39d1
186f97e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
namespace ts.server { | ||
export const dynamicImport = (id: string) => import(id); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"extends": "../tsconfig-library-base", | ||
"compilerOptions": { | ||
"outDir": "../../built/local", | ||
"rootDir": ".", | ||
"target": "esnext", | ||
"module": "esnext", | ||
"lib": ["esnext"], | ||
"declaration": false, | ||
"sourceMap": true, | ||
"tsBuildInfoFile": "../../built/local/dynamicImportCompat.tsbuildinfo" | ||
}, | ||
"files": [ | ||
"dynamicImportCompat.ts", | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -802,6 +802,9 @@ namespace ts.server { | |
|
||
private performanceEventHandler?: PerformanceEventHandler; | ||
|
||
private pendingPluginEnablements?: ESMap<Project, Promise<BeginEnablePluginResult>[]>; | ||
private currentPluginEnablementPromise?: Promise<void>; | ||
|
||
constructor(opts: ProjectServiceOptions) { | ||
this.host = opts.host; | ||
this.logger = opts.logger; | ||
|
@@ -4056,6 +4059,114 @@ namespace ts.server { | |
return false; | ||
} | ||
|
||
/*@internal*/ | ||
requestEnablePlugin(project: Project, pluginConfigEntry: PluginImport, searchPaths: string[], pluginConfigOverrides: Map<any> | undefined) { | ||
if (!this.host.importServicePlugin && !this.host.require) { | ||
this.logger.info("Plugins were requested but not running in environment that supports 'require'. Nothing will be loaded"); | ||
return; | ||
} | ||
|
||
this.logger.info(`Enabling plugin ${pluginConfigEntry.name} from candidate paths: ${searchPaths.join(",")}`); | ||
if (!pluginConfigEntry.name || parsePackageName(pluginConfigEntry.name).rest) { | ||
this.logger.info(`Skipped loading plugin ${pluginConfigEntry.name || JSON.stringify(pluginConfigEntry)} because only package name is allowed plugin name`); | ||
return; | ||
} | ||
|
||
// If the host supports dynamic import, begin enabling the plugin asynchronously. | ||
if (this.host.importServicePlugin) { | ||
const importPromise = project.beginEnablePluginAsync(pluginConfigEntry, searchPaths, pluginConfigOverrides); | ||
this.pendingPluginEnablements ??= new Map(); | ||
let promises = this.pendingPluginEnablements.get(project); | ||
if (!promises) this.pendingPluginEnablements.set(project, promises = []); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle this map when project closes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure it should matter. This map is built up during the course of a single request, and then is set to |
||
promises.push(importPromise); | ||
return; | ||
} | ||
|
||
// Otherwise, load the plugin using `require` | ||
project.endEnablePlugin(project.beginEnablePluginSync(pluginConfigEntry, searchPaths, pluginConfigOverrides)); | ||
rbuckton marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/* @internal */ | ||
hasNewPluginEnablementRequests() { | ||
return !!this.pendingPluginEnablements; | ||
} | ||
|
||
/* @internal */ | ||
hasPendingPluginEnablements() { | ||
return !!this.currentPluginEnablementPromise; | ||
} | ||
|
||
/** | ||
* Waits for any ongoing plugin enablement requests to complete. | ||
*/ | ||
/* @internal */ | ||
async waitForPendingPlugins() { | ||
while (this.currentPluginEnablementPromise) { | ||
await this.currentPluginEnablementPromise; | ||
} | ||
} | ||
|
||
/** | ||
* Starts enabling any requested plugins without waiting for the result. | ||
*/ | ||
/* @internal */ | ||
enableRequestedPlugins() { | ||
if (this.pendingPluginEnablements) { | ||
void this.enableRequestedPluginsAsync(); | ||
} | ||
} | ||
|
||
private async enableRequestedPluginsAsync() { | ||
if (this.currentPluginEnablementPromise) { | ||
// If we're already enabling plugins, wait for any existing operations to complete | ||
await this.waitForPendingPlugins(); | ||
} | ||
|
||
// Skip if there are no new plugin enablement requests | ||
if (!this.pendingPluginEnablements) { | ||
return; | ||
} | ||
|
||
// Consume the pending plugin enablement requests | ||
const entries = arrayFrom(this.pendingPluginEnablements.entries()); | ||
this.pendingPluginEnablements = undefined; | ||
|
||
// Start processing the requests, keeping track of the promise for the operation so that | ||
// project consumers can potentially wait for the plugins to load. | ||
this.currentPluginEnablementPromise = this.enableRequestedPluginsWorker(entries); | ||
await this.currentPluginEnablementPromise; | ||
} | ||
|
||
private async enableRequestedPluginsWorker(pendingPlugins: [Project, Promise<BeginEnablePluginResult>[]][]) { | ||
// This should only be called from `enableRequestedPluginsAsync`, which ensures this precondition is met. | ||
Debug.assert(this.currentPluginEnablementPromise === undefined); | ||
|
||
// Process all pending plugins, partitioned by project. This way a project with few plugins doesn't need to wait | ||
// on a project with many plugins. | ||
await Promise.all(map(pendingPlugins, ([project, promises]) => this.enableRequestedPluginsForProjectAsync(project, promises))); | ||
sheetalkamat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Clear the pending operation and notify the client that projects have been updated. | ||
this.currentPluginEnablementPromise = undefined; | ||
this.sendProjectsUpdatedInBackgroundEvent(); | ||
} | ||
|
||
private async enableRequestedPluginsForProjectAsync(project: Project, promises: Promise<BeginEnablePluginResult>[]) { | ||
// Await all pending plugin imports. This ensures all requested plugin modules are fully loaded | ||
// prior to patching the language service, and that any promise rejections are observed. | ||
const results = await Promise.all(promises); | ||
if (project.isClosed()) { | ||
// project is not alive, so don't enable plugins. | ||
return; | ||
} | ||
|
||
for (const result of results) { | ||
project.endEnablePlugin(result); | ||
} | ||
|
||
// Plugins may have modified external files, so mark the project as dirty. | ||
this.delayUpdateProjectGraph(project); | ||
} | ||
|
||
configurePlugin(args: protocol.ConfigurePluginRequestArguments) { | ||
// For any projects that already have the plugin loaded, configure the plugin | ||
this.forEachEnabledProject(project => project.onPluginConfigurationChanged(args.pluginName, args.configuration)); | ||
|
Uh oh!
There was an error while loading. Please reload this page.