task.ts 9.6 KB
Newer Older
M
Matt Bierner 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as fs from 'fs';
7
import * as jsonc from 'jsonc-parser';
M
Matt Bierner 已提交
8 9
import * as path from 'path';
import * as vscode from 'vscode';
10
import * as nls from 'vscode-nls';
11
import { ITypeScriptServiceClient } from '../typescriptService';
12
import { isTsConfigFileName } from '../utils/languageDescription';
13
import { Lazy } from '../utils/lazy';
14
import { isImplicitProjectConfigFile } from '../utils/tsconfig';
15
import TsConfigProvider, { TSConfig } from '../utils/tsconfigProvider';
M
Matt Bierner 已提交
16

17 18
const localize = nls.loadMessageBundle();

19
type AutoDetect = 'on' | 'off' | 'build' | 'watch';
20

21

M
Matt Bierner 已提交
22 23 24 25 26 27 28
const exists = (file: string): Promise<boolean> =>
	new Promise<boolean>((resolve, _reject) => {
		fs.exists(file, (value: boolean) => {
			resolve(value);
		});
	});

29

D
Dirk Baeumer 已提交
30
interface TypeScriptTaskDefinition extends vscode.TaskDefinition {
D
Dirk Baeumer 已提交
31
	tsconfig: string;
32
	option?: string;
33 34
}

35 36 37
/**
 * Provides tasks for building `tsconfig.json` files in a project.
 */
38
export default class TscTaskProvider implements vscode.TaskProvider {
39
	private autoDetect: AutoDetect = 'on';
40
	private readonly tsconfigProvider: TsConfigProvider;
41
	private readonly disposables: vscode.Disposable[] = [];
M
Matt Bierner 已提交
42 43

	public constructor(
44
		private readonly client: Lazy<ITypeScriptServiceClient>
45 46
	) {
		this.tsconfigProvider = new TsConfigProvider();
47 48 49 50 51 52 53

		vscode.workspace.onDidChangeConfiguration(this.onConfigurationChanged, this, this.disposables);
		this.onConfigurationChanged();
	}

	dispose() {
		this.disposables.forEach(x => x.dispose());
54 55 56
	}

	public async provideTasks(token: vscode.CancellationToken): Promise<vscode.Task[]> {
M
Matt Bierner 已提交
57
		const folders = vscode.workspace.workspaceFolders;
58
		if ((this.autoDetect === 'off') || !folders || !folders.length) {
M
Matt Bierner 已提交
59 60 61
			return [];
		}

62 63 64 65 66
		const configPaths: Set<string> = new Set();
		const tasks: vscode.Task[] = [];
		for (const project of await this.getAllTsConfigs(token)) {
			if (!configPaths.has(project.path)) {
				configPaths.add(project.path);
67
				tasks.push(...(await this.getTasksForProject(project)));
68 69 70
			}
		}
		return tasks;
M
Matt Bierner 已提交
71 72
	}

73
	public async resolveTask(_task: vscode.Task): Promise<vscode.Task | undefined> {
74
		const definition = <TypeScriptTaskDefinition>_task.definition;
75 76
		const badTsconfig = /\\tsconfig.*\.json/;
		if (badTsconfig.exec(definition.tsconfig) !== null) {
77
			// Warn that the task has the wrong slash type
78
			vscode.window.showWarningMessage(localize('badTsConfig', "TypeScript Task in tasks.json contains \"\\\\\". TypeScript tasks tsconfig must use \"/\""));
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
			return undefined;
		}

		const typescriptTask = (<any>_task.definition).tsconfig;
		if (typescriptTask) {
			if (_task.scope === undefined || _task.scope === vscode.TaskScope.Global || _task.scope === vscode.TaskScope.Workspace) {
				// scope is required to be a WorkspaceFolder for resolveTask
				return undefined;
			}
			const kind: TypeScriptTaskDefinition = (<any>_task.definition);
			const tsconfigUri: vscode.Uri = _task.scope.uri.with({ path: _task.scope.uri.path + '/' + kind.tsconfig });
			const tsconfig: TSConfig = {
				path: tsconfigUri.fsPath,
				posixPath: tsconfigUri.path,
				workspaceFolder: _task.scope
			};
			return this.getTasksForProjectAndDefinition(tsconfig, kind);
96
		}
D
Dirk Baeumer 已提交
97 98 99
		return undefined;
	}

100 101
	private async getAllTsConfigs(token: vscode.CancellationToken): Promise<TSConfig[]> {
		const out = new Set<TSConfig>();
102 103 104 105
		const configs = [
			...await this.getTsConfigForActiveFile(token),
			...await this.getTsConfigsInWorkspace()
		];
106
		for (const config of configs) {
107
			if (await exists(config.path)) {
108
				out.add(config);
109 110
			}
		}
111
		return Array.from(out);
112
	}
M
Matt Bierner 已提交
113

114
	private async getTsConfigForActiveFile(token: vscode.CancellationToken): Promise<TSConfig[]> {
M
Matt Bierner 已提交
115 116
		const editor = vscode.window.activeTextEditor;
		if (editor) {
117
			if (isTsConfigFileName(editor.document.fileName)) {
118
				const uri = editor.document.uri;
119
				return [{
120
					path: uri.fsPath,
121
					posixPath: uri.path,
122
					workspaceFolder: vscode.workspace.getWorkspaceFolder(uri)
123
				}];
M
Matt Bierner 已提交
124 125 126 127 128 129 130 131
			}
		}

		const file = this.getActiveTypeScriptFile();
		if (!file) {
			return [];
		}

132 133 134 135 136 137 138
		const response = await this.client.value.execute(
			'projectInfo',
			{ file, needFileNameList: false },
			token);
		if (response.type !== 'response' || !response.body) {
			return [];
		}
M
Matt Bierner 已提交
139

140 141 142 143 144 145 146
		const { configFileName } = response.body;
		if (configFileName && !isImplicitProjectConfigFile(configFileName)) {
			const normalizedConfigPath = path.normalize(configFileName);
			const uri = vscode.Uri.file(normalizedConfigPath);
			const folder = vscode.workspace.getWorkspaceFolder(uri);
			return [{
				path: normalizedConfigPath,
147
				posixPath: uri.path,
148 149
				workspaceFolder: folder
			}];
M
Matt Bierner 已提交
150
		}
151

M
Matt Bierner 已提交
152 153 154
		return [];
	}

155
	private async getTsConfigsInWorkspace(): Promise<TSConfig[]> {
156
		return Array.from(await this.tsconfigProvider.getConfigsForWorkspace());
M
Matt Bierner 已提交
157 158
	}

159
	private static async getCommand(project: TSConfig): Promise<string> {
160
		if (project.workspaceFolder) {
161 162 163 164 165 166 167 168
			const localTsc = await TscTaskProvider.getLocalTscAtPath(path.dirname(project.path));
			if (localTsc) {
				return localTsc;
			}

			const workspaceTsc = await TscTaskProvider.getLocalTscAtPath(project.workspaceFolder.uri.fsPath);
			if (workspaceTsc) {
				return workspaceTsc;
169
			}
M
Matt Bierner 已提交
170
		}
171 172

		// Use global tsc version
173
		return 'tsc';
M
Matt Bierner 已提交
174 175
	}

176 177 178 179 180 181 182 183 184 185 186
	private static async getLocalTscAtPath(folderPath: string): Promise<string | undefined> {
		const platform = process.platform;
		const bin = path.join(folderPath, 'node_modules', '.bin');
		if (platform === 'win32' && await exists(path.join(bin, 'tsc.cmd'))) {
			return path.join(bin, 'tsc.cmd');
		} else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(bin, 'tsc'))) {
			return path.join(bin, 'tsc');
		}
		return undefined;
	}

M
Matt Bierner 已提交
187
	private getActiveTypeScriptFile(): string | undefined {
M
Matt Bierner 已提交
188 189 190 191
		const editor = vscode.window.activeTextEditor;
		if (editor) {
			const document = editor.document;
			if (document && (document.languageId === 'typescript' || document.languageId === 'typescriptreact')) {
M
Matt Bierner 已提交
192
				return this.client.value.toPath(document.uri);
M
Matt Bierner 已提交
193 194
			}
		}
M
Matt Bierner 已提交
195
		return undefined;
M
Matt Bierner 已提交
196
	}
197

198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
	private getBuildTask(workspaceFolder: vscode.WorkspaceFolder | undefined, label: string, command: string, args: string[], buildTaskidentifier: TypeScriptTaskDefinition): vscode.Task {
		const buildTask = new vscode.Task(
			buildTaskidentifier,
			workspaceFolder || vscode.TaskScope.Workspace,
			localize('buildTscLabel', 'build - {0}', label),
			'tsc',
			new vscode.ShellExecution(command, args),
			'$tsc');
		buildTask.group = vscode.TaskGroup.Build;
		buildTask.isBackground = false;
		return buildTask;
	}

	private getWatchTask(workspaceFolder: vscode.WorkspaceFolder | undefined, label: string, command: string, args: string[], watchTaskidentifier: TypeScriptTaskDefinition) {
		const watchTask = new vscode.Task(
			watchTaskidentifier,
			workspaceFolder || vscode.TaskScope.Workspace,
			localize('buildAndWatchTscLabel', 'watch - {0}', label),
			'tsc',
			new vscode.ShellExecution(command, [...args, '--watch']),
			'$tsc-watch');
		watchTask.group = vscode.TaskGroup.Build;
		watchTask.isBackground = true;
		return watchTask;
	}

224
	private async getTasksForProject(project: TSConfig): Promise<vscode.Task[]> {
225
		const command = await TscTaskProvider.getCommand(project);
226
		const args = await this.getBuildShellArgs(project);
227
		const label = this.getLabelForTasks(project);
228

229 230
		const tasks: vscode.Task[] = [];

231
		if (this.autoDetect === 'build' || this.autoDetect === 'on') {
232
			tasks.push(this.getBuildTask(project.workspaceFolder, label, command, args, { type: 'typescript', tsconfig: label }));
233 234
		}

235
		if (this.autoDetect === 'watch' || this.autoDetect === 'on') {
236 237

			tasks.push(this.getWatchTask(project.workspaceFolder, label, command, args, { type: 'typescript', tsconfig: label, option: 'watch' }));
238 239 240 241 242
		}

		return tasks;
	}

243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
	private async getTasksForProjectAndDefinition(project: TSConfig, definition: TypeScriptTaskDefinition): Promise<vscode.Task | undefined> {
		const command = await TscTaskProvider.getCommand(project);
		const args = await this.getBuildShellArgs(project);
		const label = this.getLabelForTasks(project);

		let task: vscode.Task | undefined;

		if (definition.option === undefined) {
			task = this.getBuildTask(project.workspaceFolder, label, command, args, definition);
		} else if (definition.option === 'watch') {
			task = this.getWatchTask(project.workspaceFolder, label, command, args, definition);
		}

		return task;
	}

259 260 261 262 263 264 265 266 267
	private getBuildShellArgs(project: TSConfig): Promise<Array<string>> {
		const defaultArgs = ['-p', project.path];
		return new Promise<Array<string>>((resolve) => {
			fs.readFile(project.path, (error, result) => {
				if (error) {
					return resolve(defaultArgs);
				}

				try {
268
					const tsconfig = jsonc.parse(result.toString());
269 270 271 272 273 274 275 276 277 278 279
					if (tsconfig.references) {
						return resolve(['-b', project.path]);
					}
				} catch {
					// noop
				}
				return resolve(defaultArgs);
			});
		});
	}

280
	private getLabelForTasks(project: TSConfig): string {
281
		if (project.workspaceFolder) {
282 283
			const workspaceNormalizedUri = vscode.Uri.file(path.normalize(project.workspaceFolder.uri.fsPath)); // Make sure the drive letter is lowercase
			return path.posix.relative(workspaceNormalizedUri.path, project.posixPath);
284
		}
285

286
		return project.posixPath;
287
	}
288

289
	private onConfigurationChanged(): void {
290 291
		const type = vscode.workspace.getConfiguration('typescript.tsc').get<AutoDetect>('autoDetect');
		this.autoDetect = typeof type === 'undefined' ? 'on' : type;
292
	}
293
}