From 5e4d510a366fd43b8dd8d2a6ac16a75102c5c6d7 Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Fri, 14 Apr 2017 15:19:32 +0200 Subject: [PATCH] Grunt autodetection for terminal task runner --- build/npm/postinstall.js | 3 +- extensions/grunt/package.json | 48 +++++++ extensions/grunt/package.nls.json | 3 + extensions/grunt/src/main.ts | 176 +++++++++++++++++++++++++ extensions/grunt/src/typings/refs.d.ts | 8 ++ extensions/grunt/tsconfig.json | 19 +++ extensions/gulp/package.nls.json | 2 +- 7 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 extensions/grunt/package.json create mode 100644 extensions/grunt/package.nls.json create mode 100644 extensions/grunt/src/main.ts create mode 100644 extensions/grunt/src/typings/refs.d.ts create mode 100644 extensions/grunt/tsconfig.json diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 7be5aa49006..fd64909a4ee 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -32,7 +32,8 @@ const extensions = [ 'css', 'html', 'git', - 'gulp' + 'gulp', + 'grunt' ]; extensions.forEach(extension => npmInstall(`extensions/${extension}`)); diff --git a/extensions/grunt/package.json b/extensions/grunt/package.json new file mode 100644 index 00000000000..6e67c3153de --- /dev/null +++ b/extensions/grunt/package.json @@ -0,0 +1,48 @@ +{ + "name": "grunt", + "publisher": "vscode", + "description": "Extension to add Grunt capabilities to VSCode.", + "displayName": "Grunt support for VSCode", + "version": "0.0.1", + "engines": { + "vscode": "*" + }, + "enableProposedApi": true, + "categories": [ + "Other" + ], + "scripts": { + "compile": "gulp compile-extension:gulp", + "watch": "gulp watch-extension:gulp" + }, + "dependencies": { + "vscode-nls": "^2.0.2" + }, + "devDependencies": { + "@types/node": "^7.0.12" + }, + "main": "./out/main", + "activationEvents": [ + "onCommand:workbench.action.tasks.runTask", + "onCommand:workbench.action.tasks.build", + "onCommand:workbench.action.tasks.test" + ], + "contributes": { + "configuration": { + "id": "grunt", + "type": "object", + "title": "Grunt", + "properties": { + "grunt.autoDetect": { + "type": "string", + "enum": [ + "off", + "on" + ], + "default": "on", + "description": "%config.grunt.autoDetect%" + } + } + } + } +} \ No newline at end of file diff --git a/extensions/grunt/package.nls.json b/extensions/grunt/package.nls.json new file mode 100644 index 00000000000..95b1104be13 --- /dev/null +++ b/extensions/grunt/package.nls.json @@ -0,0 +1,3 @@ +{ + "config.grunt.autoDetect": "Controls whether auto detection of Grunt tasks is on or off. Default is on." +} \ No newline at end of file diff --git a/extensions/grunt/src/main.ts b/extensions/grunt/src/main.ts new file mode 100644 index 00000000000..ff551f904ec --- /dev/null +++ b/extensions/grunt/src/main.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as path from 'path'; +import * as fs from 'fs'; +import * as cp from 'child_process'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; + +const localize = nls.config(process.env.VSCODE_NLS_CONFIG)(); + +type AutoDetect = 'on' | 'off'; +let taskProvider: vscode.Disposable | undefined; + +export function activate(_context: vscode.ExtensionContext): void { + let workspaceRoot = vscode.workspace.rootPath; + if (!workspaceRoot) { + return; + } + let pattern = path.join(workspaceRoot, 'Gruntfile.js'); + let detectorPromise: Thenable | undefined = undefined; + let fileWatcher = vscode.workspace.createFileSystemWatcher(pattern); + fileWatcher.onDidChange(() => detectorPromise = undefined); + fileWatcher.onDidCreate(() => detectorPromise = undefined); + fileWatcher.onDidDelete(() => detectorPromise = undefined); + + function onConfigurationChanged() { + let autoDetect = vscode.workspace.getConfiguration('grunt').get('autoDetect'); + if (taskProvider && autoDetect === 'off') { + detectorPromise = undefined; + taskProvider.dispose(); + taskProvider = undefined; + } else if (!taskProvider && autoDetect === 'on') { + taskProvider = vscode.workspace.registerTaskProvider({ + provideTasks: () => { + if (!detectorPromise) { + detectorPromise = getGruntTasks(); + } + return detectorPromise; + } + }); + } + } + vscode.workspace.onDidChangeConfiguration(onConfigurationChanged); + onConfigurationChanged(); +} + +export function deactivate(): void { + if (taskProvider) { + taskProvider.dispose(); + } +} + +function exists(file: string): Promise { + return new Promise((resolve, _reject) => { + fs.exists(file, (value) => { + resolve(value); + }); + }); +} + +function exec(command: string, options: cp.ExecOptions): Promise<{ stdout: string; stderr: string }> { + return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { + cp.exec(command, options, (error, stdout, stderr) => { + if (error) { + reject({ error, stdout, stderr }); + } + resolve({ stdout, stderr }); + }); + }); +} + +async function getGruntTasks(): Promise { + let workspaceRoot = vscode.workspace.rootPath; + let emptyTasks: vscode.Task[] = []; + if (!workspaceRoot) { + return emptyTasks; + } + let gruntfile = path.join(workspaceRoot, 'Gruntfile.js'); + if (!await exists(gruntfile)) { + return emptyTasks; + } + + let command: string; + let platform = process.platform; + if (platform === 'win32' && await exists(path.join(workspaceRoot!, 'node_modules', '.bin', 'grunt.cmd'))) { + command = path.join('.', 'node_modules', '.bin', 'grunt.cmd'); + } else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(workspaceRoot!, 'node_modules', '.bin', 'grunt'))) { + command = path.join('.', 'node_modules', '.bin', 'grunt'); + } else { + command = 'grunt'; + } + + let commandLine = `${command} --help --no-color`; + let channel = vscode.window.createOutputChannel('tasks'); + try { + let { stdout, stderr } = await exec(commandLine, { cwd: workspaceRoot }); + if (stderr) { + channel.appendLine(stderr); + channel.show(true); + } + let result: vscode.Task[] = []; + if (stdout) { + let buildTask: { task: vscode.Task | undefined, rank: number } = { task: undefined, rank: 0 }; + let testTask: { task: vscode.Task | undefined, rank: number } = { task: undefined, rank: 0 }; + + // grunt lists tasks as follows (description is wrapped into a new line if too long): + // ... + // Available tasks + // uglify Minify files with UglifyJS. * + // jshint Validate files with JSHint. * + // test Alias for "jshint", "qunit" tasks. + // default Alias for "jshint", "qunit", "concat", "uglify" tasks. + // long Alias for "eslint", "qunit", "browserify", "sass", + // "autoprefixer", "uglify", tasks. + // + // Tasks run in the order specified + + let lines = stdout.split(/\r{0,1}\n/); + let tasksStart = false; + let tasksEnd = false; + for (let line of lines) { + if (line.length === 0) { + continue; + } + if (!tasksStart && !tasksEnd) { + if (line.indexOf('Available tasks') === 0) { + tasksStart = true; + } + } else if (tasksStart && !tasksEnd) { + if (line.indexOf('Tasks run in the order specified') === 0) { + tasksEnd = true; + } else { + let regExp = /^\s*(\S.*\S) \S/g; + let matches = regExp.exec(line); + if (matches && matches.length === 2) { + let taskName = matches[1]; + let task = taskName.indexOf(' ') === -1 + ? new vscode.ShellTask(`grunt: ${taskName}`, `${command} ${taskName}`) + : new vscode.ShellTask(`grunt: ${taskName}`, `${command} "${taskName}"`); + task.identifier = `grunt.${taskName}`; + result.push(task); + let lowerCaseTaskName = taskName.toLowerCase(); + if (lowerCaseTaskName === 'build') { + buildTask = { task, rank: 2 }; + } else if (lowerCaseTaskName.indexOf('build') !== -1 && buildTask.rank < 1) { + buildTask = { task, rank: 1 }; + } else if (lowerCaseTaskName === 'test') { + testTask = { task, rank: 2 }; + } else if (lowerCaseTaskName.indexOf('test') !== -1 && testTask.rank < 1) { + testTask = { task, rank: 1 }; + } + } + } + } + } + if (buildTask.task) { + buildTask.task.group = vscode.TaskGroup.Build; + } + if (testTask.task) { + testTask.task.group = vscode.TaskGroup.Test; + } + } + return result; + } catch (err) { + if (err.stderr) { + channel.appendLine(err.stderr); + channel.show(true); + } + channel.appendLine(localize('execFailed', 'Auto detecting Grunt failed with error: {0}', err.error ? err.error.toString() : 'unknown')); + return emptyTasks; + } +} \ No newline at end of file diff --git a/extensions/grunt/src/typings/refs.d.ts b/extensions/grunt/src/typings/refs.d.ts new file mode 100644 index 00000000000..954bab971e3 --- /dev/null +++ b/extensions/grunt/src/typings/refs.d.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// diff --git a/extensions/grunt/tsconfig.json b/extensions/grunt/tsconfig.json new file mode 100644 index 00000000000..d815a565579 --- /dev/null +++ b/extensions/grunt/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "lib": [ + "es2016" + ], + "outDir": "./out", + "strictNullChecks": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "exclude": [ + "node_modules", + "out" + ] +} \ No newline at end of file diff --git a/extensions/gulp/package.nls.json b/extensions/gulp/package.nls.json index 716f83aa90a..15df4d03b03 100644 --- a/extensions/gulp/package.nls.json +++ b/extensions/gulp/package.nls.json @@ -1,3 +1,3 @@ { - "config.gulp.autoDetect": "Controls whether auto detection of gulp tasks in on or off. Default is on." + "config.gulp.autoDetect": "Controls whether auto detection of Gulp tasks is on or off. Default is on." } \ No newline at end of file