diff --git a/extensions/npm/README.md b/extensions/npm/README.md index 4bca043bc4a..e52ac4bf7ea 100644 --- a/extensions/npm/README.md +++ b/extensions/npm/README.md @@ -11,3 +11,4 @@ To run scripts as tasks you use the `Tasks` menu. - `npm.autoDetect` enable detecting scripts as tasks, the default is `on`. - `npm.runSilent` run npm script with the `--silent` option, the default is `false`. - `npm.packageManager` the package manager used to run the scripts: `npm` or `yarn`, the default is `npm`. +- `npm.exclude` glob patterns for folders that should be excluded from automatic script detection. The pattern is matched against the **absolute path** of the package.json. For example to exclude all test folders user '**/test/**. diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 2997fb03f59..ba3ae2f7f45 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -17,10 +17,12 @@ }, "dependencies": { "jsonc-parser": "^1.0.0", + "minimatch": "^3.0.4", "request-light": "^0.2.2", "vscode-nls": "^3.2.1" }, "devDependencies": { + "@types/minimatch": "^3.0.3", "@types/node": "7.0.43" }, "main": "./out/main", @@ -59,6 +61,17 @@ ], "default": "npm", "description": "%config.npm.packageManager%" + }, + "npm.exclude": { + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + }, + "description": "%config.npm.exclude%", + "scope": "window" } } }, @@ -91,4 +104,4 @@ } ] } -} \ No newline at end of file +} diff --git a/extensions/npm/package.nls.json b/extensions/npm/package.nls.json index 5850ad89ad2..0a9ece35f6e 100644 --- a/extensions/npm/package.nls.json +++ b/extensions/npm/package.nls.json @@ -4,6 +4,7 @@ "config.npm.autoDetect": "Controls whether auto detection of npm scripts is on or off. Default is on.", "config.npm.runSilent": "Run npm commands with the `--silent` option.", "config.npm.packageManager": "The package manager used to run scripts.", + "config.npm.exclude": "Configure glob patterns for folders that should be excluded from automatic script detection.", "npm.parseError": "Npm task detection: failed to parse the file {0}", "taskdef.script": "The npm script to customize.", "taskdef.path": "The path to the folder of the package.json file that provides the script. Can be ommitted." diff --git a/extensions/npm/src/main.ts b/extensions/npm/src/main.ts index a115bf315d7..ad57992cc88 100644 --- a/extensions/npm/src/main.ts +++ b/extensions/npm/src/main.ts @@ -9,6 +9,8 @@ import * as fs from 'fs'; import * as httpRequest from 'request-light'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import * as minimatch from 'minimatch'; + const localize = nls.loadMessageBundle(); import { addJSONProviders } from './features/jsonContributions'; @@ -97,15 +99,17 @@ function isNotPreOrPostScript(script: string): boolean { async function provideNpmScripts(): Promise { let emptyTasks: vscode.Task[] = []; let allTasks: vscode.Task[] = []; - let folders = vscode.workspace.workspaceFolders; - if (!folders) { + let paths = await vscode.workspace.findFiles('**/package.json', '**/node_modules/**'); + if (paths.length === 0) { return emptyTasks; } + try { - for (let i = 0; i < folders.length; i++) { - if (isEnabled(folders[i])) { - let tasks = await provideNpmScriptsForFolder(folders[i]); + for (let i = 0; i < paths.length; i++) { + let folder = vscode.workspace.getWorkspaceFolder(paths[i]); + if (folder && isEnabled(folder) && !isExcluded(folder, paths[i])) { + let tasks = await provideNpmScriptsForFolder(paths[i]); allTasks.push(...tasks); } } @@ -119,19 +123,45 @@ function isEnabled(folder: vscode.WorkspaceFolder): boolean { return vscode.workspace.getConfiguration('npm', folder.uri).get('autoDetect') === 'on'; } -async function provideNpmScriptsForFolder(folder: vscode.WorkspaceFolder): Promise { +function isExcluded(folder: vscode.WorkspaceFolder, packageJsonUri: vscode.Uri) { + function testForExclusionPattern(path: string, pattern: string): boolean { + return minimatch(path, pattern, { dot: true }); + } + + let exclude = vscode.workspace.getConfiguration('npm', folder.uri).get('exclude'); + + if (exclude) { + if (Array.isArray(exclude)) { + for (let pattern of exclude) { + if (testForExclusionPattern(packageJsonUri.fsPath, pattern)) { + return true; + } + } + } else if (testForExclusionPattern(packageJsonUri.fsPath, exclude)) { + return true; + } + } + return false; +} + +async function provideNpmScriptsForFolder(packageJsonUri: vscode.Uri): Promise { let emptyTasks: vscode.Task[] = []; - if (folder.uri.scheme !== 'file') { + if (packageJsonUri.scheme !== 'file') { return emptyTasks; } - let rootPath = folder.uri.fsPath; - let packageJson = path.join(rootPath, 'package.json'); + let packageJson = packageJsonUri.fsPath; + if (!await exists(packageJson)) { return emptyTasks; } + let folder = vscode.workspace.getWorkspaceFolder(packageJsonUri); + if (!folder) { + return emptyTasks; + } + try { var contents = await readFile(packageJson); var json = JSON.parse(contents); @@ -141,7 +171,7 @@ async function provideNpmScriptsForFolder(folder: vscode.WorkspaceFolder): Promi const result: vscode.Task[] = []; Object.keys(json.scripts).filter(isNotPreOrPostScript).forEach(each => { - const task = createTask(each, `run ${each}`, rootPath, folder); + const task = createTask(each, `run ${each}`, folder!, packageJsonUri); const lowerCaseTaskName = each.toLowerCase(); if (isBuildTask(lowerCaseTaskName)) { task.group = vscode.TaskGroup.Build; @@ -154,14 +184,17 @@ async function provideNpmScriptsForFolder(folder: vscode.WorkspaceFolder): Promi // result.push(createTask('install', 'install', rootPath, folder, [])); return result; } catch (e) { - let localizedParseError = localize('npm.parseError', 'Npm task detection: failed to parse the file {0}', packageJson); + let localizedParseError = localize('npm.parseError', 'Npm task detection: failed to parse the file {0}', packageJsonUri); throw new Error(localizedParseError); } } -function createTask(script: string, cmd: string, rootPath: string, folder: vscode.WorkspaceFolder, matcher?: any): vscode.Task { +function createTask(script: string, cmd: string, folder: vscode.WorkspaceFolder, packageJsonUri: vscode.Uri, matcher?: any): vscode.Task { - function getTaskName(script: string) { + function getTaskName(script: string, file: string) { + if (file.length) { + return `${script} - ${file.substring(0, file.length - 1)}`; + } return script; } @@ -173,10 +206,21 @@ function createTask(script: string, cmd: string, rootPath: string, folder: vscod return `${packageManager} ${cmd}`; } + function getRelativePath(folder: vscode.WorkspaceFolder, packageJsonUri: vscode.Uri): string { + let rootUri = folder.uri; + let absolutePath = packageJsonUri.fsPath; + return absolutePath.substring(rootUri.fsPath.length + 1, absolutePath.length - 'package.json'.length); + } + let kind: NpmTaskDefinition = { type: 'npm', script: script }; - let taskName = getTaskName(script); - return new vscode.Task(kind, folder, taskName, 'npm', new vscode.ShellExecution(getCommandLine(folder, cmd), { cwd: rootPath }), matcher); + let relativePackageJson = getRelativePath(folder, packageJsonUri); + if (relativePackageJson.length) { + kind.file = getRelativePath(folder, packageJsonUri); + } + let taskName = getTaskName(script, relativePackageJson); + let cwd = path.dirname(packageJsonUri.fsPath); + return new vscode.Task(kind, folder, taskName, 'npm', new vscode.ShellExecution(getCommandLine(folder, cmd), { cwd: cwd }), matcher); } diff --git a/extensions/npm/yarn.lock b/extensions/npm/yarn.lock index 2b88873cecc..741a6fe9a22 100644 --- a/extensions/npm/yarn.lock +++ b/extensions/npm/yarn.lock @@ -2,6 +2,10 @@ # yarn lockfile v1 +"@types/minimatch@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + "@types/node@7.0.43": version "7.0.43" resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c" @@ -12,6 +16,21 @@ agent-base@4, agent-base@^4.1.0: dependencies: es6-promisify "^5.0.0" +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + debug@2: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -52,6 +71,12 @@ jsonc-parser@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-1.0.1.tgz#7f8f296414e6e7c4a33b9e4914fc8c47e4421675" +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"