diff --git a/src/diagnostics.ts b/src/diagnostics.ts
index ca239d370..bf0397262 100644
--- a/src/diagnostics.ts
+++ b/src/diagnostics.ts
@@ -18,7 +18,7 @@ export function convertTsDiagnostic(diagnostic: ts.Diagnostic): Diagnostic {
 		message: text,
 		severity: convertDiagnosticCategory(diagnostic.category),
 		code: diagnostic.code,
-		source: 'ts'
+		source: diagnostic.source || 'ts'
 	};
 }
 
diff --git a/src/match-files.ts b/src/match-files.ts
index f3b45202e..6d38ba38d 100644
--- a/src/match-files.ts
+++ b/src/match-files.ts
@@ -55,7 +55,7 @@ export function matchFiles(path: string, extensions: string[], excludes: string[
 
 const directorySeparator = '/';
 
-function combinePaths(path1: string, path2: string) {
+export function combinePaths(path1: string, path2: string) {
 	if (!(path1 && path1.length)) return path2;
 	if (!(path2 && path2.length)) return path1;
 	if (getRootLength(path2) !== 0) return path2;
diff --git a/src/plugins.ts b/src/plugins.ts
new file mode 100644
index 000000000..4221a18f0
--- /dev/null
+++ b/src/plugins.ts
@@ -0,0 +1,196 @@
+import * as fs from 'mz/fs';
+import * as path from 'path';
+import * as ts from 'typescript';
+import { Logger, NoopLogger } from './logging';
+import { combinePaths } from './match-files';
+import { PluginSettings } from './request-type';
+import { toUnixPath } from './util';
+
+// Based on types and logic from TypeScript server/project.ts @
+// https://github.com/Microsoft/TypeScript/blob/711e890e59e10aa05a43cb938474a3d9c2270429/src/server/project.ts
+
+/**
+ * A plugin exports an initialization function, injected with
+ * the current typescript instance
+ */
+export type PluginModuleFactory = (mod: { typescript: typeof ts }) => PluginModule;
+
+export type EnableProxyFunc = (pluginModuleFactory: PluginModuleFactory, pluginConfigEntry: ts.PluginImport) => void;
+
+/**
+ * A plugin presents this API when initialized
+ */
+export interface PluginModule {
+	create(createInfo: PluginCreateInfo): ts.LanguageService;
+	getExternalFiles?(proj: Project): string[];
+}
+
+/**
+ * All of tsserver's environment exposed to plugins
+ */
+export interface PluginCreateInfo {
+	project: Project;
+	languageService: ts.LanguageService;
+	languageServiceHost: ts.LanguageServiceHost;
+	serverHost: ServerHost;
+	config: any;
+}
+
+/**
+ * The portion of tsserver's Project API exposed to plugins
+ */
+export interface Project {
+	projectService: {
+		logger: Logger;
+	};
+}
+
+/**
+ * The portion of tsserver's ServerHost API exposed to plugins
+ */
+export type ServerHost = object;
+
+/**
+ * The result of a node require: a module or an error.
+ */
+type RequireResult = { module: {}, error: undefined } | { module: undefined, error: {} };
+
+export class PluginLoader {
+
+	private allowLocalPluginLoads: boolean = false;
+	private globalPlugins: string[] = [];
+	private pluginProbeLocations: string[] = [];
+
+	constructor(
+		private rootFilePath: string,
+		private fs: ts.ModuleResolutionHost,
+		pluginSettings?: PluginSettings,
+		private logger = new NoopLogger(),
+		private resolutionHost = new LocalModuleResolutionHost(),
+		private requireModule: (moduleName: string) => any = require) {
+		if (pluginSettings) {
+			this.allowLocalPluginLoads = pluginSettings.allowLocalPluginLoads || false;
+			this.globalPlugins = pluginSettings.globalPlugins || [];
+			this.pluginProbeLocations = pluginSettings.pluginProbeLocations || [];
+		}
+	}
+
+	public loadPlugins(options: ts.CompilerOptions, applyProxy: EnableProxyFunc) {
+		// Search our peer node_modules, then any globally-specified probe paths
+		// ../../.. to walk from X/node_modules/javascript-typescript-langserver/lib/project-manager.js to X/node_modules/
+		const searchPaths = [combinePaths(__filename, '../../..'), ...this.pluginProbeLocations];
+
+		// Corresponds to --allowLocalPluginLoads, opt-in to avoid remote code execution.
+		if (this.allowLocalPluginLoads) {
+			const local = this.rootFilePath;
+			this.logger.info(`Local plugin loading enabled; adding ${local} to search paths`);
+			searchPaths.unshift(local);
+		}
+
+		let pluginImports: ts.PluginImport[] = [];
+		if (options.plugins) {
+			pluginImports = options.plugins as ts.PluginImport[];
+		}
+
+		// Enable tsconfig-specified plugins
+		if (options.plugins) {
+			for (const pluginConfigEntry of pluginImports) {
+				this.enablePlugin(pluginConfigEntry, searchPaths, applyProxy);
+			}
+		}
+
+		if (this.globalPlugins) {
+			// Enable global plugins with synthetic configuration entries
+			for (const globalPluginName of this.globalPlugins) {
+				// Skip already-locally-loaded plugins
+				if (!pluginImports || pluginImports.some(p => p.name === globalPluginName)) {
+					continue;
+				}
+
+				// Provide global: true so plugins can detect why they can't find their config
+				this.enablePlugin({ name: globalPluginName, global: true } as ts.PluginImport, searchPaths, applyProxy);
+			}
+		}
+	}
+
+	/**
+	 * Tries to load and enable a single plugin
+	 * @param pluginConfigEntry
+	 * @param searchPaths
+	 */
+	private enablePlugin(pluginConfigEntry: ts.PluginImport, searchPaths: string[], enableProxy: EnableProxyFunc) {
+		for (const searchPath of searchPaths) {
+			const resolvedModule =  this.resolveModule(pluginConfigEntry.name, searchPath) as PluginModuleFactory;
+			if (resolvedModule) {
+				enableProxy(resolvedModule, pluginConfigEntry);
+				return;
+			}
+		}
+		this.logger.error(`Couldn't find ${pluginConfigEntry.name} anywhere in paths: ${searchPaths.join(',')}`);
+	}
+
+	/**
+	 * Load a plugin using a node require
+	 * @param moduleName
+	 * @param initialDir
+	 */
+	private resolveModule(moduleName: string, initialDir: string): {} | undefined {
+		const resolvedPath = toUnixPath(path.resolve(combinePaths(initialDir, 'node_modules')));
+		this.logger.info(`Loading ${moduleName} from ${initialDir} (resolved to ${resolvedPath})`);
+		const result = this.requirePlugin(resolvedPath, moduleName);
+		if (result.error) {
+			this.logger.error(`Failed to load module: ${JSON.stringify(result.error)}`);
+			return undefined;
+		}
+		return result.module;
+	}
+
+	/**
+	 * Resolves a loads a plugin function relative to initialDir
+	 * @param initialDir
+	 * @param moduleName
+	 */
+	private requirePlugin(initialDir: string, moduleName: string): RequireResult {
+		try {
+			const modulePath = this.resolveJavaScriptModule(moduleName, initialDir, this.fs);
+			return { module: this.requireModule(modulePath), error: undefined };
+		} catch (error) {
+			return { module: undefined, error };
+		}
+	}
+
+	/**
+	 * Expose resolution logic to allow us to use Node module resolution logic from arbitrary locations.
+	 * No way to do this with `require()`: https://github.com/nodejs/node/issues/5963
+	 * Throws an error if the module can't be resolved.
+	 * stolen from moduleNameResolver.ts because marked as internal
+	 */
+	private resolveJavaScriptModule(moduleName: string, initialDir: string, host: ts.ModuleResolutionHost): string {
+		// TODO: this should set jsOnly=true to the internal resolver, but this parameter is not exposed on a public api.
+		const result =
+			ts.nodeModuleNameResolver(
+				moduleName,
+				initialDir.replace('\\', '/') + '/package.json', /* containingFile */
+				{ moduleResolution: ts.ModuleResolutionKind.NodeJs, allowJs: true },
+				this.resolutionHost,
+				undefined
+			);
+		if (!result.resolvedModule) {
+			// this.logger.error(result.failedLookupLocations);
+			throw new Error(`Could not resolve JS module ${moduleName} starting at ${initialDir}.`);
+		}
+		return result.resolvedModule.resolvedFileName;
+	}
+}
+
+/**
+ * A local filesystem-based ModuleResolutionHost for plugin loading.
+ */
+export class LocalModuleResolutionHost implements ts.ModuleResolutionHost {
+	fileExists(fileName: string): boolean {
+		return fs.existsSync(fileName);
+	}
+	readFile(fileName: string): string {
+		return fs.readFileSync(fileName, 'utf8');
+	}
+}
diff --git a/src/project-manager.ts b/src/project-manager.ts
index 96831cc41..dd5dbf680 100644
--- a/src/project-manager.ts
+++ b/src/project-manager.ts
@@ -9,6 +9,8 @@ import { Disposable } from './disposable';
 import { FileSystemUpdater } from './fs';
 import { Logger, NoopLogger } from './logging';
 import { InMemoryFileSystem } from './memfs';
+import { PluginCreateInfo, PluginLoader, PluginModuleFactory } from './plugins';
+import { PluginSettings } from './request-type';
 import { traceObservable, traceSync } from './tracing';
 import {
 	isConfigFile,
@@ -100,6 +102,11 @@ export class ProjectManager implements Disposable {
 	 */
 	private subscriptions = new Subscription();
 
+	/**
+	 * Options passed to the language server at startup
+	 */
+	private pluginSettings?: PluginSettings;
+
 	/**
 	 * @param rootPath root path as passed to `initialize`
 	 * @param inMemoryFileSystem File system that keeps structure and contents in memory
@@ -111,12 +118,14 @@ export class ProjectManager implements Disposable {
 		inMemoryFileSystem: InMemoryFileSystem,
 		updater: FileSystemUpdater,
 		traceModuleResolution?: boolean,
+		pluginSettings?: PluginSettings,
 		protected logger: Logger = new NoopLogger()
 	) {
 		this.rootPath = rootPath;
 		this.updater = updater;
 		this.inMemoryFs = inMemoryFileSystem;
 		this.versions = new Map<string, number>();
+		this.pluginSettings = pluginSettings;
 		this.traceModuleResolution = traceModuleResolution || false;
 
 		// Share DocumentRegistry between all ProjectConfigurations
@@ -144,6 +153,7 @@ export class ProjectManager implements Disposable {
 				'',
 				tsConfig,
 				this.traceModuleResolution,
+				this.pluginSettings,
 				this.logger
 			);
 			configs.set(trimmedRootPath, config);
@@ -173,6 +183,7 @@ export class ProjectManager implements Disposable {
 						filePath,
 						undefined,
 						this.traceModuleResolution,
+						this.pluginSettings,
 						this.logger
 					));
 					// Remove catch-all config (if exists)
@@ -802,6 +813,7 @@ export class ProjectConfiguration {
 		configFilePath: string,
 		configContent?: any,
 		traceModuleResolution?: boolean,
+		private pluginSettings?: PluginSettings,
 		private logger: Logger = new NoopLogger()
 	) {
 		this.fs = fs;
@@ -910,9 +922,38 @@ export class ProjectConfiguration {
 			this.logger
 		);
 		this.service = ts.createLanguageService(this.host, this.documentRegistry);
+		const pluginLoader = new PluginLoader(this.rootFilePath, this.fs, this.pluginSettings, this.logger);
+		pluginLoader.loadPlugins(options, (factory, config) => this.wrapService(factory, config));
 		this.initialized = true;
 	}
 
+	/**
+	 * Replaces the LanguageService with an instance wrapped by the plugin
+	 * @param pluginModuleFactory function to create the module
+	 * @param configEntry extra settings from tsconfig to pass to the plugin module
+	 */
+	private wrapService(pluginModuleFactory: PluginModuleFactory, configEntry: ts.PluginImport) {
+		try {
+			if (typeof pluginModuleFactory !== 'function') {
+				this.logger.info(`Skipped loading plugin ${configEntry.name} because it didn't expose a proper factory function`);
+				return;
+			}
+
+			const info: PluginCreateInfo = {
+				config: configEntry,
+				project: { projectService: { logger: this.logger }}, // TODO: may need more support
+				languageService: this.getService(),
+				languageServiceHost: this.getHost(),
+				serverHost: {} // TODO: may need an adapter
+			};
+
+			const pluginModule = pluginModuleFactory({ typescript: ts });
+			this.service = pluginModule.create(info);
+		} catch (e) {
+			this.logger.error(`Plugin activation failed: ${e}`);
+		}
+	}
+
 	/**
 	 * Ensures we are ready to process files from a given sub-project
 	 */
diff --git a/src/request-type.ts b/src/request-type.ts
index 7cbd3c3e0..e2f8dd435 100644
--- a/src/request-type.ts
+++ b/src/request-type.ts
@@ -5,6 +5,15 @@ export interface InitializeParams extends vscode.InitializeParams {
 	capabilities: ClientCapabilities;
 }
 
+/**
+ * Settings to enable plugin loading
+ */
+export interface PluginSettings {
+	allowLocalPluginLoads: boolean;
+	globalPlugins: string[];
+	pluginProbeLocations: string[];
+}
+
 export interface ClientCapabilities extends vscode.ClientCapabilities {
 
 	/**
diff --git a/src/test/plugins.test.ts b/src/test/plugins.test.ts
new file mode 100644
index 000000000..ea4037f59
--- /dev/null
+++ b/src/test/plugins.test.ts
@@ -0,0 +1,69 @@
+import * as path from 'path';
+import * as sinon from 'sinon';
+import * as ts from 'typescript';
+import {InMemoryFileSystem} from '../memfs';
+import {PluginLoader, PluginModule, PluginModuleFactory} from '../plugins';
+import {PluginSettings} from '../request-type';
+import { path2uri } from '../util';
+
+describe('plugins', () => {
+	describe('loadPlugins()', () => {
+		it('should do nothing if no plugins are configured', () => {
+			const memfs = new InMemoryFileSystem('/');
+
+			const loader = new PluginLoader('/', memfs);
+			const compilerOptions: ts.CompilerOptions = {};
+			const applyProxy: (pluginModuleFactory: PluginModuleFactory) => PluginModule = sinon.spy();
+			loader.loadPlugins(compilerOptions, applyProxy);
+
+		});
+
+		it('should load a global plugin if specified', () => {
+			const memfs = new InMemoryFileSystem('/');
+			const peerPackagesPath = path.resolve(__filename, '../../../../');
+			const peerPackagesUri = path2uri(peerPackagesPath);
+			memfs.add(peerPackagesUri + '/node_modules/some-plugin/package.json', '{ "name": "some-plugin", "version": "0.1.1", "main": "plugin.js"}');
+			memfs.add(peerPackagesUri + '/node_modules/some-plugin/plugin.js', '');
+			const pluginSettings: PluginSettings = {
+				globalPlugins: ['some-plugin'],
+				allowLocalPluginLoads: false,
+				pluginProbeLocations: []
+			};
+			const pluginFactoryFunc = (modules: any) => 5;
+			const fakeRequire = (path: string) => pluginFactoryFunc;
+			const loader = new PluginLoader('/', memfs, pluginSettings, undefined, memfs, fakeRequire);
+			const compilerOptions: ts.CompilerOptions = {};
+			const applyProxy = sinon.spy();
+			loader.loadPlugins(compilerOptions, applyProxy);
+			sinon.assert.calledOnce(applyProxy);
+			sinon.assert.calledWithExactly(applyProxy, pluginFactoryFunc, sinon.match({ name: 'some-plugin', global: true}));
+		});
+
+		it('should load a local plugin if specified', () => {
+			const rootDir = (process.platform === 'win32' ? 'c:\\' : '/') + 'some-project';
+			const rootUri = path2uri(rootDir) + '/';
+			const memfs = new InMemoryFileSystem('/some-project');
+			memfs.add(rootUri + 'node_modules/some-plugin/package.json', '{ "name": "some-plugin", "version": "0.1.1", "main": "plugin.js"}');
+			memfs.add(rootUri + 'node_modules/some-plugin/plugin.js', '');
+			const pluginSettings: PluginSettings = {
+				globalPlugins: [],
+				allowLocalPluginLoads: true,
+				pluginProbeLocations: []
+			};
+			const pluginFactoryFunc = (modules: any) => 5;
+			const fakeRequire = (path: string) => pluginFactoryFunc;
+			const loader = new PluginLoader(rootDir, memfs, pluginSettings, undefined, memfs, fakeRequire);
+			const pluginOption: ts.PluginImport = {
+				name: 'some-plugin'
+			};
+			const compilerOptions: ts.CompilerOptions = {
+				plugins: [pluginOption]
+			};
+			const applyProxy = sinon.spy();
+			loader.loadPlugins(compilerOptions, applyProxy);
+			sinon.assert.calledOnce(applyProxy);
+			sinon.assert.calledWithExactly(applyProxy, pluginFactoryFunc, sinon.match(pluginOption));
+		});
+
+	});
+});
diff --git a/src/typescript-service.ts b/src/typescript-service.ts
index fb1030452..4c6e6f116 100644
--- a/src/typescript-service.ts
+++ b/src/typescript-service.ts
@@ -49,6 +49,7 @@ import {
 	InitializeResult,
 	PackageDescriptor,
 	PackageInformation,
+	PluginSettings,
 	ReferenceInformation,
 	SymbolDescriptor,
 	SymbolLocationInformation,
@@ -86,7 +87,7 @@ export type TypeScriptServiceFactory = (client: LanguageClient, options?: TypeSc
 /**
  * Settings synced through `didChangeConfiguration`
  */
-export interface Settings {
+export interface Settings extends PluginSettings {
 	format: ts.FormatCodeSettings;
 }
 
@@ -171,7 +172,10 @@ export class TypeScriptService {
 			insertSpaceBeforeFunctionParenthesis: false,
 			placeOpenBraceOnNewLineForFunctions: false,
 			placeOpenBraceOnNewLineForControlBlocks: false
-		}
+		},
+		allowLocalPluginLoads: false,
+		globalPlugins: [],
+		pluginProbeLocations: []
 	};
 
 	/**
@@ -223,6 +227,7 @@ export class TypeScriptService {
 				this.inMemoryFileSystem,
 				this.updater,
 				this.traceModuleResolution,
+				this.settings,
 				this.logger
 			);
 			this.packageManager = new PackageManager(this.updater, this.inMemoryFileSystem, this.logger);
@@ -311,7 +316,7 @@ export class TypeScriptService {
 	 * A notification sent from the client to the server to signal the change of configuration
 	 * settings.
 	 */
-	didChangeConfiguration(params: DidChangeConfigurationParams): void {
+	workspaceDidChangeConfiguration(params: DidChangeConfigurationParams): void {
 		merge(this.settings, params.settings);
 	}
 
@@ -1337,15 +1342,8 @@ export class TypeScriptService {
 		if (!config) {
 			return;
 		}
-		const program = config.getProgram(span);
-		if (!program) {
-			return;
-		}
-		const sourceFile = program.getSourceFile(uri2path(uri));
-		if (!sourceFile) {
-			return;
-		}
-		const tsDiagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
+		const fileName = uri2path(uri);
+		const tsDiagnostics = config.getService().getSyntacticDiagnostics(fileName).concat(config.getService().getSemanticDiagnostics(fileName));
 		const diagnostics = iterate(tsDiagnostics)
 			// TS can report diagnostics without a file and range in some cases
 			// These cannot be represented as LSP Diagnostics since the range and URI is required