tasks.ts 9.4 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
E
Erich Gamma 已提交
5
'use strict';
E
Erich Gamma 已提交
6

7
import { TaskDefinition, Task, TaskGroup, WorkspaceFolder, RelativePattern, ShellExecution, Uri, workspace } from 'vscode';
E
Erich Gamma 已提交
8 9
import * as path from 'path';
import * as fs from 'fs';
10
import * as minimatch from 'minimatch';
J
Joao Moreno 已提交
11
import * as nls from 'vscode-nls';
E
Erich Gamma 已提交
12
import { JSONVisitor, visit, ParseErrorCode } from 'jsonc-parser/lib/main';
J
Joao Moreno 已提交
13 14

const localize = nls.loadMessageBundle();
E
Erich Gamma 已提交
15 16 17 18 19 20

export interface NpmTaskDefinition extends TaskDefinition {
	script: string;
	path?: string;
}

21 22
type AutoDetect = 'on' | 'off';

23 24 25 26 27 28
let cachedTasks: Task[] | undefined = undefined;

export function invalidateScriptsCache() {
	cachedTasks = undefined;
}

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
const buildNames: string[] = ['build', 'compile', 'watch'];
function isBuildTask(name: string): boolean {
	for (let buildName of buildNames) {
		if (name.indexOf(buildName) !== -1) {
			return true;
		}
	}
	return false;
}

const testNames: string[] = ['test'];
function isTestTask(name: string): boolean {
	for (let testName of testNames) {
		if (name === testName) {
			return true;
		}
	}
	return false;
}

49 50 51 52 53 54 55 56 57 58 59 60
function getPrePostScripts(scripts: any): Set<string> {
	const prePostScripts: Set<string> = new Set([
		'preuninstall', 'postuninstall', 'prepack', 'postpack', 'preinstall', 'postinstall',
		'prepack', 'postpack', 'prepublish', 'postpublish', 'preversion', 'postversion',
		'prestop', 'poststop', 'prerestart', 'postrestart', 'preshrinkwrap', 'postshrinkwrap',
		'pretest', 'postest', 'prepublishOnly'
	]);
	let keys = Object.keys(scripts);
	for (let i = 0; i < keys.length; i++) {
		const script = keys[i];
		const prepost = ['pre' + script, 'post' + script];
		prepost.forEach(each => {
E
Erich Gamma 已提交
61
			if (scripts[each] !== undefined) {
62 63 64 65 66
				prePostScripts.add(each);
			}
		});
	}
	return prePostScripts;
67 68
}

E
Erich Gamma 已提交
69 70
export function isWorkspaceFolder(value: any): value is WorkspaceFolder {
	return value && typeof value !== 'number';
E
Erich Gamma 已提交
71 72
}

73 74 75 76
export function getPackageManager(folder: WorkspaceFolder): string {
	return workspace.getConfiguration('npm', folder.uri).get<string>('packageManager', 'npm');
}

77 78 79 80 81 82 83 84
export async function hasNpmScripts(): Promise<boolean> {
	let folders = workspace.workspaceFolders;
	if (!folders) {
		return false;
	}
	try {
		for (let i = 0; i < folders.length; i++) {
			let folder = folders[i];
85
			if (isAutoDetectionEnabled(folder)) {
86 87 88 89 90 91 92 93 94 95 96 97 98
				let relativePattern = new RelativePattern(folder, '**/package.json');
				let paths = await workspace.findFiles(relativePattern, '**/node_modules/**');
				if (paths.length > 0) {
					return true;
				}
			}
		}
		return false;
	} catch (error) {
		return Promise.reject(error);
	}
}

99 100
async function detectNpmScripts(): Promise<Task[]> {

101 102
	let emptyTasks: Task[] = [];
	let allTasks: Task[] = [];
103
	let visitedPackageJsonFiles: Set<string> = new Set();
104 105 106 107 108 109 110 111

	let folders = workspace.workspaceFolders;
	if (!folders) {
		return emptyTasks;
	}
	try {
		for (let i = 0; i < folders.length; i++) {
			let folder = folders[i];
112
			if (isAutoDetectionEnabled(folder)) {
113 114 115
				let relativePattern = new RelativePattern(folder, '**/package.json');
				let paths = await workspace.findFiles(relativePattern, '**/node_modules/**');
				for (let j = 0; j < paths.length; j++) {
116 117 118 119
					let path = paths[j];
					if (!isExcluded(folder, path) && !visitedPackageJsonFiles.has(path.fsPath)) {
						let tasks = await provideNpmScriptsForFolder(path);
						visitedPackageJsonFiles.add(path.fsPath);
120 121 122 123 124 125 126 127 128 129 130
						allTasks.push(...tasks);
					}
				}
			}
		}
		return allTasks;
	} catch (error) {
		return Promise.reject(error);
	}
}

131 132 133 134 135 136 137
export async function provideNpmScripts(): Promise<Task[]> {
	if (!cachedTasks) {
		cachedTasks = await detectNpmScripts();
	}
	return cachedTasks;
}

138
function isAutoDetectionEnabled(folder: WorkspaceFolder): boolean {
139 140 141 142 143 144 145 146 147
	return workspace.getConfiguration('npm', folder.uri).get<AutoDetect>('autoDetect') === 'on';
}

function isExcluded(folder: WorkspaceFolder, packageJsonUri: Uri) {
	function testForExclusionPattern(path: string, pattern: string): boolean {
		return minimatch(path, pattern, { dot: true });
	}

	let exclude = workspace.getConfiguration('npm', folder.uri).get<string | string[]>('exclude');
148
	let packageJsonFolder = path.dirname(packageJsonUri.fsPath);
149 150 151 152

	if (exclude) {
		if (Array.isArray(exclude)) {
			for (let pattern of exclude) {
153
				if (testForExclusionPattern(packageJsonFolder, pattern)) {
154 155 156
					return true;
				}
			}
157
		} else if (testForExclusionPattern(packageJsonFolder, exclude)) {
158 159 160 161 162 163
			return true;
		}
	}
	return false;
}

164 165 166 167 168
function isDebugScript(script: string): boolean {
	let match = script.match(/--(inspect|debug)(-brk)?(=(\d*))?/);
	return match !== null;
}

J
Joao Moreno 已提交
169
async function provideNpmScriptsForFolder(packageJsonUri: Uri): Promise<Task[]> {
170 171 172 173 174 175
	let emptyTasks: Task[] = [];

	let folder = workspace.getWorkspaceFolder(packageJsonUri);
	if (!folder) {
		return emptyTasks;
	}
J
Joao Moreno 已提交
176
	let scripts = await getScripts(packageJsonUri);
177 178 179 180 181
	if (!scripts) {
		return emptyTasks;
	}

	const result: Task[] = [];
182

E
Erich Gamma 已提交
183 184
	const prePostScripts = getPrePostScripts(scripts);
	Object.keys(scripts).forEach(each => {
185 186 187 188 189 190 191
		const task = createTask(each, `run ${each}`, folder!, packageJsonUri);
		const lowerCaseTaskName = each.toLowerCase();
		if (isBuildTask(lowerCaseTaskName)) {
			task.group = TaskGroup.Build;
		} else if (isTestTask(lowerCaseTaskName)) {
			task.group = TaskGroup.Test;
		}
E
Erich Gamma 已提交
192 193 194
		if (prePostScripts.has(each)) {
			task.group = TaskGroup.Clean; // hack: use Clean group to tag pre/post scripts
		}
195 196 197
		if (isDebugScript(scripts![each])) {
			task.group = TaskGroup.Rebuild; // hack: use Rebuild group to tag debug scripts
		}
198 199 200
		result.push(task);
	});
	// always add npm install (without a problem matcher)
201
	result.push(createTask('install', 'install', folder, packageJsonUri, []));
202 203 204
	return result;
}

E
Erich Gamma 已提交
205 206 207
export function getTaskName(script: string, relativePath: string | undefined) {
	if (relativePath && relativePath.length) {
		return `${script} - ${relativePath.substring(0, relativePath.length - 1)}`;
208
	}
E
Erich Gamma 已提交
209 210 211
	return script;
}

212
export function createTask(script: string, cmd: string, folder: WorkspaceFolder, packageJsonUri: Uri, matcher?: any): Task {
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241

	function getCommandLine(folder: WorkspaceFolder, cmd: string): string {
		let packageManager = getPackageManager(folder);
		if (workspace.getConfiguration('npm', folder.uri).get<boolean>('runSilent')) {
			return `${packageManager} --silent ${cmd}`;
		}
		return `${packageManager} ${cmd}`;
	}

	function getRelativePath(folder: WorkspaceFolder, packageJsonUri: Uri): string {
		let rootUri = folder.uri;
		let absolutePath = packageJsonUri.path.substring(0, packageJsonUri.path.length - 'package.json'.length);
		return absolutePath.substring(rootUri.path.length + 1);
	}

	let kind: NpmTaskDefinition = {
		type: 'npm',
		script: script
	};
	let relativePackageJson = getRelativePath(folder, packageJsonUri);
	if (relativePackageJson.length) {
		kind.path = getRelativePath(folder, packageJsonUri);
	}
	let taskName = getTaskName(script, relativePackageJson);
	let cwd = path.dirname(packageJsonUri.fsPath);
	return new Task(kind, folder, taskName, 'npm', new ShellExecution(getCommandLine(folder, cmd), { cwd: cwd }), matcher);
}


E
Erich Gamma 已提交
242 243 244 245 246 247 248 249 250 251 252
export function getPackageJsonUriFromTask(task: Task): Uri | null {
	if (isWorkspaceFolder(task.scope)) {
		if (task.definition.path) {
			return Uri.file(path.join(task.scope.uri.fsPath, task.definition.path, 'package.json'));
		} else {
			return Uri.file(path.join(task.scope.uri.fsPath, 'package.json'));
		}
	}
	return null;
}

E
Erich Gamma 已提交
253
async function exists(file: string): Promise<boolean> {
E
Erich Gamma 已提交
254 255 256 257 258 259 260
	return new Promise<boolean>((resolve, _reject) => {
		fs.exists(file, (value) => {
			resolve(value);
		});
	});
}

E
Erich Gamma 已提交
261
async function readFile(file: string): Promise<string> {
E
Erich Gamma 已提交
262 263 264 265 266 267 268 269 270 271
	return new Promise<string>((resolve, reject) => {
		fs.readFile(file, (err, data) => {
			if (err) {
				reject(err);
			}
			resolve(data.toString());
		});
	});
}

E
Erich Gamma 已提交
272 273 274 275 276 277 278 279 280
export type StringMap = { [s: string]: string; };

async function findAllScripts(buffer: string): Promise<StringMap> {
	var scripts: StringMap = {};
	let script: string | undefined = undefined;
	let inScripts = false;

	let visitor: JSONVisitor = {
		onError(_error: ParseErrorCode, _offset: number, _length: number) {
281
			// TODO: inform user about the parse error
E
Erich Gamma 已提交
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
		},
		onObjectEnd() {
			if (inScripts) {
				inScripts = false;
			}
		},
		onLiteralValue(value: any, _offset: number, _length: number) {
			if (script) {
				scripts[script] = value;
				script = undefined;
			}
		},
		onObjectProperty(property: string, _offset: number, _length: number) {
			if (property === 'scripts') {
				inScripts = true;
			}
			else if (inScripts) {
				script = property;
			}
		}
	};
	visit(buffer, visitor);
	return scripts;
}

export async function getScripts(packageJsonUri: Uri): Promise<StringMap | undefined> {
E
Erich Gamma 已提交
308 309

	if (packageJsonUri.scheme !== 'file') {
E
Erich Gamma 已提交
310
		return undefined;
E
Erich Gamma 已提交
311 312 313 314
	}

	let packageJson = packageJsonUri.fsPath;
	if (!await exists(packageJson)) {
E
Erich Gamma 已提交
315
		return undefined;
E
Erich Gamma 已提交
316 317 318 319
	}

	try {
		var contents = await readFile(packageJson);
E
Erich Gamma 已提交
320 321
		var json = findAllScripts(contents);//JSON.parse(contents);
		return json;
E
Erich Gamma 已提交
322
	} catch (e) {
E
Erich Gamma 已提交
323
		let localizedParseError = localize('npm.parseError', 'Npm task detection: failed to parse the file {0}', packageJsonUri.fsPath);
E
Erich Gamma 已提交
324 325 326
		throw new Error(localizedParseError);
	}
}