From d7e901b10cacd4644492b4ed996da905135a3df3 Mon Sep 17 00:00:00 2001 From: Aldo Mendoza Saucedo Date: Wed, 17 May 2017 14:07:45 -0700 Subject: [PATCH] Update npm task with install/publish/custom commands --- .../resources.resjson/en-US/resources.resjson | 52 +++- Tasks/Npm/Tests/L0.ts | 264 +++++++++++++----- Tasks/Npm/Tests/NpmMockHelper.ts | 183 ++++++------ Tasks/Npm/Tests/config-noDebug.ts | 19 ++ Tasks/Npm/Tests/custom-version.ts | 19 ++ Tasks/Npm/Tests/install-feed.ts | 22 ++ Tasks/Npm/Tests/install-npmFailure.ts | 17 ++ Tasks/Npm/Tests/install-npmrc.ts | 20 ++ Tasks/Npm/Tests/publish-external.ts | 30 ++ Tasks/Npm/Tests/publish-feed.ts | 23 ++ .../test-commandContainsSpaces-deprecated.ts | 12 - Tasks/Npm/Tests/test-commandContainsSpaces.ts | 10 - ...test-commandWithoutArguments-deprecated.ts | 30 -- .../Npm/Tests/test-commandWithoutArguments.ts | 29 -- Tasks/Npm/Tests/test-configlist-deprecated.ts | 28 -- Tasks/Npm/Tests/test-configlist.ts | 27 -- Tasks/Npm/Tests/test-npmFailure-deprecated.ts | 33 --- Tasks/Npm/Tests/test-npmFailure.ts | 32 --- Tasks/Npm/constants.ts | 23 ++ Tasks/Npm/make.json | 12 - Tasks/Npm/npm.ts | 37 +++ Tasks/Npm/npmcustom.ts | 48 ++++ Tasks/Npm/npminstall.ts | 9 + Tasks/Npm/npmpublish.ts | 44 +++ Tasks/Npm/npmrcparser.ts | 30 ++ Tasks/Npm/npmregistry.ts | 72 +++++ Tasks/Npm/npmtask.ts | 248 ---------------- Tasks/Npm/npmtoolrunner.ts | 77 +++++ Tasks/Npm/package.json | 5 +- Tasks/Npm/task.json | 159 +++++++++-- Tasks/Npm/task.loc.json | 157 +++++++++-- Tasks/Npm/typings.json | 4 +- Tasks/Npm/typings/index.d.ts | 2 + Tasks/Npm/typings/modules/ini/index.d.ts | 20 ++ Tasks/Npm/typings/modules/ini/typings.json | 8 + Tasks/Npm/typings/modules/mockery/index.d.ts | 36 +++ .../Npm/typings/modules/mockery/typings.json | 8 + Tasks/Npm/util.ts | 166 +++++++++++ 38 files changed, 1320 insertions(+), 695 deletions(-) create mode 100644 Tasks/Npm/Tests/config-noDebug.ts create mode 100644 Tasks/Npm/Tests/custom-version.ts create mode 100644 Tasks/Npm/Tests/install-feed.ts create mode 100644 Tasks/Npm/Tests/install-npmFailure.ts create mode 100644 Tasks/Npm/Tests/install-npmrc.ts create mode 100644 Tasks/Npm/Tests/publish-external.ts create mode 100644 Tasks/Npm/Tests/publish-feed.ts delete mode 100644 Tasks/Npm/Tests/test-commandContainsSpaces-deprecated.ts delete mode 100644 Tasks/Npm/Tests/test-commandContainsSpaces.ts delete mode 100644 Tasks/Npm/Tests/test-commandWithoutArguments-deprecated.ts delete mode 100644 Tasks/Npm/Tests/test-commandWithoutArguments.ts delete mode 100644 Tasks/Npm/Tests/test-configlist-deprecated.ts delete mode 100644 Tasks/Npm/Tests/test-configlist.ts delete mode 100644 Tasks/Npm/Tests/test-npmFailure-deprecated.ts delete mode 100644 Tasks/Npm/Tests/test-npmFailure.ts create mode 100644 Tasks/Npm/constants.ts create mode 100644 Tasks/Npm/npm.ts create mode 100644 Tasks/Npm/npmcustom.ts create mode 100644 Tasks/Npm/npminstall.ts create mode 100644 Tasks/Npm/npmpublish.ts create mode 100644 Tasks/Npm/npmrcparser.ts create mode 100644 Tasks/Npm/npmregistry.ts delete mode 100644 Tasks/Npm/npmtask.ts create mode 100644 Tasks/Npm/npmtoolrunner.ts create mode 100644 Tasks/Npm/typings/modules/ini/index.d.ts create mode 100644 Tasks/Npm/typings/modules/ini/typings.json create mode 100644 Tasks/Npm/typings/modules/mockery/index.d.ts create mode 100644 Tasks/Npm/typings/modules/mockery/typings.json create mode 100644 Tasks/Npm/util.ts diff --git a/Tasks/Npm/Strings/resources.resjson/en-US/resources.resjson b/Tasks/Npm/Strings/resources.resjson/en-US/resources.resjson index 9b4ec889cee7..cc50be6ebcbf 100644 --- a/Tasks/Npm/Strings/resources.resjson/en-US/resources.resjson +++ b/Tasks/Npm/Strings/resources.resjson/en-US/resources.resjson @@ -1,17 +1,43 @@ { "loc.friendlyName": "npm", "loc.helpMarkDown": "[More Information](https://go.microsoft.com/fwlink/?LinkID=613746)", - "loc.description": "Run an npm command", + "loc.description": "Install and publish npm packages, or run an npm command. Supports npmjs.com and authenticated registries like Package Management.", "loc.instanceNameFormat": "npm $(command)", - "loc.input.label.cwd": "working folder", - "loc.input.help.cwd": "Working directory where the npm command is run. Defaults to the root of the repo.", - "loc.input.label.command": "npm command", - "loc.input.label.arguments": "arguments", - "loc.input.help.arguments": "Additional arguments passed to npm.", - "loc.messages.InvalidCommand": "Only one command should be used for npm. Use 'Arguments' input for additional arguments.", - "loc.messages.NpmReturnCode": "npm exited with return code: %d", - "loc.messages.NpmFailed": "npm failed with error: %s", - "loc.messages.NpmConfigFailed": "Failed to log the npm config information. Error : %s", - "loc.messages.NpmAuthFailed": "Failed to get the required authentication tokens for npm. Error: %s", - "loc.messages.BuildCredentialsWarn": "Could not determine credentials to use for npm" -} \ No newline at end of file + "loc.group.displayName.customRegistries": "Custom registries and authentication", + "loc.group.displayName.publishRegistries": "Destination registry and authentication", + "loc.input.label.command": "Command", + "loc.input.help.command": "The command and arguments which will be passed to npm for execution.", + "loc.input.label.workingDir": "Working folder with package.json", + "loc.input.help.workingDir": "Path to the folder containing the target package.json and .npmrc files. Select the folder, not the file e.g. \"/packages/mypackage\".", + "loc.input.label.customCommand": "Command and arguments", + "loc.input.help.customCommand": "Custom command to run, e.g. \"dist-tag ls mypackage\".", + "loc.input.label.customRegistry": "Registries to use", + "loc.input.help.customRegistry": "You can either commit a .npmrc file to your source code repository and set its path here or select a registry from VSTS here.", + "loc.input.label.customFeed": "Use packages from this VSTS/TFS registry", + "loc.input.help.customFeed": "Include the selected feed in the generated .npmrc.", + "loc.input.label.customEndpoint": "Credentials for registries outside this account/collection", + "loc.input.help.customEndpoint": "Credentials to use for external registries located in the project's .npmrc. For registries in this account/collection, leave this blank; the build’s credentials are used automatically.", + "loc.input.label.publishRegistry": "Registry location", + "loc.input.help.publishRegistry": "Registry the command will target.", + "loc.input.label.publishFeed": "Target registry", + "loc.input.help.publishFeed": "Select a registry hosted in this account. You must have Package Management installed and licensed to select a registry here.", + "loc.input.label.publishEndpoint": "External Registry", + "loc.input.help.publishEndpoint": "Credentials to use for publishing to an external registry.", + "loc.messages.FoundBuildCredentials": "Found build credentials", + "loc.messages.NoBuildCredentials": "Could not find build credentials", + "loc.messages.UnknownCommand": "Unknown command: %s", + "loc.messages.MultipleProjectConfigs": "More than one project .npmrc found in $s", + "loc.messages.ServiceEndpointNotDefined": "Couldn't find Service Endpoint, make sure the selected endpoint still exists.", + "loc.messages.ServiceEndpointUrlNotDefined": "Couldn't find Url for Service Endpoint, make sure Service Endpoint is correctly configured.", + "loc.messages.SavingFile": "Saving file %s", + "loc.messages.RestoringFile": "Restoring file %s", + "loc.messages.PublishFeed": "Publishing to internal feed", + "loc.messages.PublishExternal": "Publishing to external registry", + "loc.messages.UseFeed": "Using internal feed", + "loc.messages.UseNpmrc": "Using registries in .npmrc", + "loc.messages.PublishRegistry": "Publishing to registry: %s", + "loc.messages.UsingRegistry": "Using registry: %s", + "loc.messages.AddingAuthRegistry": "Adding auth for registry: %s", + "loc.messages.FoundLocalRegistries": "Found %d registries in this account/collection", + "loc.messages.ForcePackagingUrl": "Packaging collection url forced to: %s" +} diff --git a/Tasks/Npm/Tests/L0.ts b/Tasks/Npm/Tests/L0.ts index 46770768425a..b34b930bcf22 100644 --- a/Tasks/Npm/Tests/L0.ts +++ b/Tasks/Npm/Tests/L0.ts @@ -1,138 +1,256 @@ -import * as path from 'path'; import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as Q from 'q'; + +import * as mockery from 'mockery'; import * as ttm from 'vsts-task-lib/mock-test'; -import {NpmMockHelper} from './NpmMockHelper'; + +import { NpmMockHelper } from './NpmMockHelper'; describe('Npm Task', function () { before(() => { + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: false + } as mockery.MockeryEnableArgs); }); after(() => { + mockery.disable(); + }); + + beforeEach(() => { + mockery.resetCache(); + }); + + afterEach(() => { + mockery.deregisterAll(); }); - /* Current behavior */ - it("should execute 'npm config list' successfully", (done: MochaDone) => { + // custom + it('custom command should return npm version', (done: MochaDone) => { this.timeout(1000); - let tp = path.join(__dirname, 'test-configlist.js') - let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + let tp = path.join(__dirname, 'custom-version.js'); + let tr = new ttm.MockTestRunner(tp); tr.run(); - assert.equal(tr.invokedToolCount, 3, 'should have run vsts-npm-auth, npm config list and npm command'); - assert(tr.ran(`${NpmMockHelper.NpmCmdPath} config list`), 'it should have run npm'); - assert(tr.stdOutContained('; cli configs'), "should have npm config output"); - assert.equal(tr.errorIssues.length, 0, "should have no errors"); - // This assert is skipped due to a test mocking issue on non windows platforms. - // assert.equal(tr.warningIssues.length, 0, "should have no warnings: " + tr.warningIssues.join(',')); - assert(tr.succeeded, 'should have succeeded'); + assert.equal(tr.invokedToolCount, 2, 'task should have run npm'); + assert(tr.succeeded, 'task should have succeeded'); + assert(tr.stdOutContained('; debug cli configs'), 'should have debug npm config output'); + assert(tr.stdOutContained('; cli configs') === false, 'should not have regular npm config output'); done(); }); - - it('should pass when no arguments are supplied', (done: MochaDone) => { + + // show config + it('should execute \'npm config list\' without debug switch', (done: MochaDone) => { this.timeout(1000); - let tp = path.join(__dirname, 'test-commandWithoutArguments.js') - let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + let tp = path.join(__dirname, 'config-noDebug.js'); + let tr = new ttm.MockTestRunner(tp); tr.run(); - assert.equal(tr.invokedToolCount, 3, 'should have run npm'); - assert(tr.ran(`${NpmMockHelper.NpmCmdPath} root`), 'it should have run npm'); - assert(tr.stdOutContained(`${NpmMockHelper.FakeWorkingDirectory}`), "should have npm root output - working directory"); - assert(tr.stdOutContained("node_modules"), "should have npm root output - 'node_modules' directory"); - assert.equal(tr.errorIssues.length, 0, "should have no errors"); - // This assert is skipped due to a test mocking issue on non windows platforms. - // assert.equal(tr.warningIssues.length, 0, "should have no warnings: " + tr.warningIssues.join(',')); - assert(tr.succeeded, 'should have succeeded'); + assert.equal(tr.invokedToolCount, 2, 'task should have run npm'); + assert(tr.succeeded, 'task should have succeeded'); + assert(tr.stdOutContained('; cli configs'), 'should have regular npm config output'); + assert(tr.stdOutContained('; debug cli configs') === false, 'should not have debug npm config output'); done(); }); - - it('should fail when command contains spaces', (done: MochaDone) => { + + // install command + it('should fail when npm fails', (done: MochaDone) => { this.timeout(1000); - let tp = path.join(__dirname, 'test-commandContainsSpaces.js') - let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + let tp = path.join(__dirname, 'install-npmFailure.js'); + let tr = new ttm.MockTestRunner(tp); tr.run(); - assert.equal(tr.invokedToolCount, 0, 'should not have run npm'); - assert(tr.failed, 'should have failed'); + assert(tr.failed, 'task should have failed'); done(); }); - - it('should fail when task fails', (done: MochaDone) => { + + it ('install using local feed', (done: MochaDone) => { this.timeout(1000); - let tp = path.join(__dirname, 'test-npmFailure.js') - let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + let tp = path.join(__dirname, 'install-feed.js'); + let tr = new ttm.MockTestRunner(tp); tr.run(); - assert.equal(tr.invokedToolCount, 3, 'should have run npm'); - assert(tr.failed, 'should have failed'); + assert.equal(tr.invokedToolCount, 2, 'task should have run npm'); + assert(tr.stdOutContained('npm install successful'), 'npm should have installed the package'); + assert(tr.succeeded, 'task should have succeeded'); done(); }); - /* Deprecated behavior */ - it("should execute 'npm config list' successfully (deprecated task)", (done: MochaDone) => { + it ('install using npmrc', (done: MochaDone) => { this.timeout(1000); - let tp = path.join(__dirname, 'test-configlist-deprecated.js') - let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + let tp = path.join(__dirname, 'install-npmrc.js'); + let tr = new ttm.MockTestRunner(tp); tr.run(); - assert.equal(tr.invokedToolCount, 1, 'should have run npm'); - assert(tr.ran(`${NpmMockHelper.NpmCmdPath} config list`), 'it should have run npm'); - assert(tr.stdOutContained('; cli configs'), "should have npm config output"); - assert.equal(tr.errorIssues.length, 0, "should have no errors"); - assert.equal(tr.warningIssues.length, 0, "should have no warnings: " + tr.warningIssues.join(',')); - assert(tr.succeeded, 'should have succeeded'); + assert.equal(tr.invokedToolCount, 2, 'task should have run npm'); + assert(tr.stdOutContained('npm install successful'), 'npm should have installed the package'); + assert(tr.succeeded, 'task should have succeeded'); done(); }); - - it('should pass when no arguments are supplied (deprecated task)', (done: MochaDone) => { + + // publish + it ('publish using feed', (done: MochaDone) => { this.timeout(1000); - let tp = path.join(__dirname, 'test-commandWithoutArguments-deprecated.js') - let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + let tp = path.join(__dirname, 'publish-feed.js'); + let tr = new ttm.MockTestRunner(tp); tr.run(); - assert.equal(tr.invokedToolCount, 1, 'should have run npm'); - assert(tr.ran(`${NpmMockHelper.NpmCmdPath} root`), 'it should have run npm'); - assert(tr.stdOutContained(`${NpmMockHelper.FakeWorkingDirectory}`), "should have npm root output - working directory"); - assert(tr.stdOutContained("node_modules"), "should have npm root output - 'node_modules' directory"); - assert.equal(tr.errorIssues.length, 0, "should have no errors"); - assert.equal(tr.warningIssues.length, 0, "should have no warnings: " + tr.warningIssues.join(',')); - assert(tr.succeeded, 'should have succeeded'); + assert.equal(tr.invokedToolCount, 2, 'task should have run npm'); + assert(tr.stdOutContained('npm publish successful'), 'npm should have installed the package'); + assert(tr.succeeded, 'task should have succeeded'); done(); }); - - it('should fail when command contains spaces (deprecated task)', (done: MochaDone) => { + + it ('publish using external registry', (done: MochaDone) => { this.timeout(1000); - let tp = path.join(__dirname, 'test-commandContainsSpaces-deprecated.js') - let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + let tp = path.join(__dirname, 'publish-external.js'); + let tr = new ttm.MockTestRunner(tp); tr.run(); - assert.equal(tr.invokedToolCount, 0, 'should not have run npm'); - assert(tr.failed, 'should have failed'); + assert.equal(tr.invokedToolCount, 2, 'task should have run npm'); + assert(tr.stdOutContained('npm publish successful'), 'npm should have installed the package'); + assert(tr.succeeded, 'task should have succeeded'); done(); }); - - it('should fail when task fails (deprecated task)', (done: MochaDone) => { - this.timeout(1000); - let tp = path.join(__dirname, 'test-npmFailure-deprecated.js') - let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); - tr.run(); + // util + it('gets npm registries', (done: MochaDone) => { + let mockTask = { + writeFile: (file: string, data: string | Buffer) => { + // no-op + } + }; + mockery.registerMock('vsts-task-lib/task', mockTask); + let npmrc = `registry=http://example.com + always-auth=true + @scoped:registry=http://scoped.com + //scoped.com/:_authToken=thisIsASecretToken + @scopedTwo:registry=http://scopedTwo.com + ; some comments + @scoped:always-auth=true + # more comments`; + + let mockFs = { + readFileSync: (path: string) => npmrc + }; + mockery.registerMock('fs', mockFs); - assert.equal(tr.invokedToolCount, 1, 'should have run npm'); - assert(tr.failed, 'should have failed'); + let npmrcParser = require('../npmrcparser'); + let registries = npmrcParser.GetRegistries(''); + + assert.equal(registries.length, 3); + assert.equal(registries[0], 'http://example.com/'); + assert.equal(registries[1], 'http://scoped.com/'); + assert.equal(registries[2], 'http://scopedTwo.com/'); + + done(); + }); + + it('gets feed id from VSTS registry', (done: MochaDone) => { + mockery.registerMock('vsts-task-lib/task', {}); + let util = require('../util'); + + assert.equal(util.getFeedIdFromRegistry( + 'http://account.visualstudio.com/_packaging/feedId/npm/registry'), + 'feedId'); + assert.equal(util.getFeedIdFromRegistry( + 'http://account.visualstudio.com/_packaging/feedId/npm/registry/'), + 'feedId'); + assert.equal(util.getFeedIdFromRegistry( + 'http://TFSSERVER/_packaging/feedId/npm/registry'), + 'feedId'); + assert.equal(util.getFeedIdFromRegistry( + 'http://TFSSERVER:1234/_packaging/feedId/npm/registry'), + 'feedId'); done(); }); -}); \ No newline at end of file + + it('gets correct packaging Url', () => { + let mockTask = { + getVariable: (v: string) => { + if (v === 'System.TeamFoundationCollectionUri') { + return 'http://example.visualstudio.com'; + } + }, + debug: (message: string) => { + // no-op + }, + loc: (key: string) => { + // no-op + } + }; + mockery.registerMock('vsts-task-lib/task', mockTask); + let util = require('../util'); + + return util.getPackagingCollectionUrl().then(u => { + assert.equal(u, 'http://example.pkgs.visualstudio.com/'.toLowerCase()); + + mockTask.getVariable = (v: string) => 'http://TFSSERVER.com/'; + return util.getPackagingCollectionUrl().then(u => { + assert.equal(u, 'http://TFSSERVER.com/'.toLowerCase()); + + mockTask.getVariable = (v: string) => 'http://serverWithPort:1234'; + return util.getPackagingCollectionUrl().then(u => { + assert.equal(u, 'http://serverWithPort:1234/'.toLowerCase()); + + return; + }); + }); + }); + }); + + it('gets correct local registries', () => { + let mockParser = { + GetRegistries: (npmrc: string) => [ + 'http://registry.com/npmRegistry/', + 'http://example.pkgs.visualstudio.com/npmRegistry/', + 'http://localTFSServer/npmRegistry/' + ] + }; + mockery.registerMock('./npmrcparser', mockParser); + let mockTask = { + getVariable: (v: string) => { + if (v === 'System.TeamFoundationCollectionUri') { + return 'http://example.visualstudio.com'; + } + }, + debug: (message: string) => { + // no-op + }, + loc: (key: string) => { + // no-op + } + }; + mockery.registerMock('vsts-task-lib/task', mockTask); + let util = require('../util'); + + return util.getLocalRegistries('').then((registries: string[]) => { + assert.equal(registries.length, 1); + assert.equal(registries[0], 'http://example.pkgs.visualstudio.com/npmRegistry/'); + + mockTask.getVariable = () => 'http://localTFSServer/'; + return util.getLocalRegistries('').then((registries: string[]) => { + assert.equal(registries.length, 1); + assert.equal(registries[0], 'http://localTFSServer/npmRegistry/'); + }); + }); + }); +}); diff --git a/Tasks/Npm/Tests/NpmMockHelper.ts b/Tasks/Npm/Tests/NpmMockHelper.ts index 1b4138eec058..3c6ecfc7df84 100644 --- a/Tasks/Npm/Tests/NpmMockHelper.ts +++ b/Tasks/Npm/Tests/NpmMockHelper.ts @@ -1,125 +1,126 @@ -import ma = require('vsts-task-lib/mock-answer'); -import path = require('path'); -import tmrm = require('vsts-task-lib/mock-run'); - -export class NpmMockHelper { - static NpmCmdPath = "C:\\Program Files (x86)\\nodejs\\npm"; - static NpmAuthPath = "C:\\tool\\vsts-npm-auth\\vsts-npm-auth.exe"; - static FakeWorkingDirectory = "fake\\wd"; - static AgentBuildDirectory = 'c:\\agent\\work\\build'; - static BuildBuildId = '12345'; - - public answers: ma.TaskLibAnswers = { - which: {}, - exec: {}, - checkPath: {}, - exist: {}, - filter: {}, - find: {}, - match: {} - }; +import * as path from 'path'; - constructor( - private tmr: tmrm.TaskMockRunner, - public command: string, - public args: string) { - NpmMockHelper.setVariable('Agent.HomeDirectory', 'c:\\agent\\home\\directory'); - NpmMockHelper.setVariable('Build.SourcesDirectory', 'c:\\agent\\home\\directory\\sources'); - process.env['ENDPOINT_AUTH_SYSTEMVSSCONNECTION'] = "{\"parameters\":{\"AccessToken\":\"token\"},\"scheme\":\"OAuth\"}"; - process.env['ENDPOINT_URL_SYSTEMVSSCONNECTION'] = "https://example.visualstudio.com/defaultcollection"; - NpmMockHelper.setVariable('System.DefaultWorkingDirectory', 'c:\\agent\\home\\directory'); - NpmMockHelper.setVariable('System.TeamFoundationCollectionUri', 'https://example.visualstudio.com/defaultcollection'); - NpmMockHelper.setVariable('Agent.BuildDirectory', NpmMockHelper.AgentBuildDirectory); - NpmMockHelper.setVariable('Build.BuildId', NpmMockHelper.BuildBuildId); - - tmr.setInput('cwd', NpmMockHelper.FakeWorkingDirectory); - tmr.setInput('command', command); - tmr.setInput('arguments', args); - - this.setDefaultAnswers(); - } +import { TaskLibAnswers, TaskLibAnswerExecResult } from 'vsts-task-lib/mock-answer'; +import { TaskMockRunner } from 'vsts-task-lib/mock-run'; +import * as mtr from 'vsts-task-lib/mock-toolrunner'; - public run(result?: ma.TaskLibAnswerExecResult) { - if (result) { - let command = `${NpmMockHelper.NpmCmdPath} ${this.command}`; - if (this.args) { - command += " " + this.args; - } - this.setExecResponse(command, result); - } - this.tmr.setAnswers(this.answers); - this.tmr.run(); - } +export class NpmMockHelper extends TaskMockRunner { + private static NpmCmdPath: string = 'c:\\mock\\location\\npm'; + private static AgentBuildDirectory: string = 'c:\\mock\\agent\\work\\build'; + private static BuildBuildId: string = '12345'; + private static CollectionUrl: string = 'https://example.visualstudio.com/defaultcollection'; - public useDeprecatedTask() { - process.env['USE_DEPRECATED_TASK_VERSION'] = 'true'; - } + public answers: TaskLibAnswers = { + checkPath: {}, + exec: {}, + exist: {}, + rmRF: {}, + which: {} + }; - public mockAuthHelper() { - let npmTaskDirName = path.dirname(__dirname); - let authHelperExternalPath = path.join(npmTaskDirName, 'Npm', 'vsts-npm-auth'); - let authHelperExePath = path.join(authHelperExternalPath, 'bin', 'vsts-npm-auth.exe'); - this.answers.find[authHelperExternalPath] = [npmTaskDirName, authHelperExePath, authHelperExePath + ".config"]; - this.answers.filter['vsts-npm-auth.exe'] = [authHelperExePath]; - - let targetNpmrcFile = `${NpmMockHelper.AgentBuildDirectory}\\npm\\auth.${NpmMockHelper.BuildBuildId}.npmrc`; - let sourceNpmrcFile = `${NpmMockHelper.FakeWorkingDirectory}\\.npmrc`; - let command = `${authHelperExePath} -NonInteractive -Verbosity Detailed -Config ${sourceNpmrcFile} -TargetConfig ${targetNpmrcFile}`; - this.setExecResponse(command, { code: 0, stdout: "", stderr: "" }); + constructor(taskPath: string) { + super(taskPath); + + this.registerMock('vsts-task-lib/toolrunner', mtr); + this.setAnswers(this.answers); + + NpmMockHelper._setVariable('Agent.HomeDirectory', 'c:\\agent\\home\\directory'); + NpmMockHelper._setVariable('Build.SourcesDirectory', 'c:\\agent\\home\\directory\\sources'); + process.env['ENDPOINT_AUTH_SYSTEMVSSCONNECTION'] = '{"parameters":{"AccessToken":"token"},"scheme":"OAuth"}'; + process.env['ENDPOINT_URL_SYSTEMVSSCONNECTION'] = NpmMockHelper.CollectionUrl; + NpmMockHelper._setVariable('System.DefaultWorkingDirectory', 'c:\\agent\\home\\directory'); + NpmMockHelper._setVariable('System.TeamFoundationCollectionUri', 'https://example.visualstudio.com/defaultcollection'); + NpmMockHelper._setVariable('Agent.BuildDirectory', NpmMockHelper.AgentBuildDirectory); + NpmMockHelper._setVariable('Build.BuildId', NpmMockHelper.BuildBuildId); + this.setDebugState(false); + + this._mockNpmConfigList(); + this._setToolPath('npm', NpmMockHelper.NpmCmdPath); + this.answers.rmRF[path.join(NpmMockHelper.AgentBuildDirectory, 'npm', `${NpmMockHelper.BuildBuildId}.npmrc`)] = { success: true }; + this.answers.rmRF[path.join(NpmMockHelper.AgentBuildDirectory, 'npm')] = { success: true }; } - public mockNpmConfigList() { - let command = `${NpmMockHelper.NpmCmdPath} config list`; - if (this.isDebugging()) { - // add option to dump all default values - command += " -l"; - } - this.setExecResponse(command, { code: 0, stdout: "; cli configs", stderr: "" }); + public run(noMockTask?: boolean): void { + super.run(noMockTask); } - public setDebugState(isDebugging: boolean) { - NpmMockHelper.setVariable('system.debug', isDebugging ? 'true' : 'false'); + public setDebugState(debug: boolean): void { + NpmMockHelper._setVariable('System.Debug', debug ? 'true' : 'false'); } public setOsType(osTypeVal : string) { - if(!this.answers['osType']) { + if (!this.answers['osType']) { this.answers['osType'] = {}; } this.answers['osType']['osType'] = osTypeVal; } - private static setVariable(name: string, value: string) { - let key = NpmMockHelper.getVariableKey(name); + private static _setVariable(name: string, value: string): void { + let key = NpmMockHelper._getVariableKey(name); process.env[key] = value; } - private static getVariableKey(name: string) { - let key = name.replace(/\./g, '_').toUpperCase(); - return key; + private static _getVariableKey(name: string): string { + return name.replace(/\./g, '_').toUpperCase(); } - private setExecResponse(command: string, result:ma.TaskLibAnswerExecResult) { + public setExecResponse(command: string, result: TaskLibAnswerExecResult) { this.answers.exec[command] = result; } + public RegisterLocationServiceMocks() { + this.registerMock('vso-node-api/WebApi', { + getBearerHandler: function(token){ + return {}; + }, + WebApi: function(url, handler){ + return { + getCoreApi: function() { + return { + vsoClient: { + getVersioningData: function (ApiVersion: string, PackagingAreaName: string, PackageAreaId: string, Obj) { + return { requestUrl: 'foobar' }; + } + } + }; + } + }; + } + }); + } + + public mockServiceEndpoint(endpointId: string, url: string, auth: any): void { + process.env['ENDPOINT_URL_' + endpointId] = url; + process.env['ENDPOINT_AUTH_' + endpointId] = JSON.stringify(auth); + } + private isDebugging() { - let value = process.env[NpmMockHelper.getVariableKey('system.debug')]; + let value = process.env[NpmMockHelper._getVariableKey('System.Debug')]; return value === 'true'; } - private setDefaultAnswers() { - this.setToolPath(this.answers, "npm", NpmMockHelper.NpmCmdPath); - this.setOsType('WiNdOWs_nT'); - this.setProjectNpmrcExists(); + private _setToolPath(tool: string, path: string) { + this.answers.which[tool] = path; + this.answers.checkPath[path] = true; + } + + private _mockNpmConfigList() { + this.setExecResponse(`${NpmMockHelper.NpmCmdPath} config list`, { + code: 0, + stdout: '; cli configs'} as TaskLibAnswerExecResult); + + this.setExecResponse(`${NpmMockHelper.NpmCmdPath} config list -l`, { + code: 0, + stdout: '; debug cli configs'} as TaskLibAnswerExecResult); } - private setToolPath(answers: ma.TaskLibAnswers, tool: string, path: string) { - answers.which[tool] = path; - answers.checkPath[path] = true; + private _registerMockToolRunner() { + let tmr = require('vsts-task-lib/mock-toolrunner'); + this.registerMock('vsts-task-lib/toolrunner', tmr); } - private setProjectNpmrcExists() { - this.answers.exist[path.join(NpmMockHelper.FakeWorkingDirectory, '.npmrc')] = true; + private _mockGetFeedRegistryUrl(feedId: string): string { + return NpmMockHelper.CollectionUrl + '/_packaging/' + feedId + '/npm/registry/'; } -} \ No newline at end of file +} diff --git a/Tasks/Npm/Tests/config-noDebug.ts b/Tasks/Npm/Tests/config-noDebug.ts new file mode 100644 index 000000000000..6470727f07a0 --- /dev/null +++ b/Tasks/Npm/Tests/config-noDebug.ts @@ -0,0 +1,19 @@ +import * as path from 'path'; + +import { TaskLibAnswerExecResult } from 'vsts-task-lib/mock-answer'; +import * as tmrm from 'vsts-task-lib/mock-run'; + +import { NpmMockHelper } from './NpmMockHelper'; + +let taskPath = path.join(__dirname, '..', 'npm.js'); +let tmr = new NpmMockHelper(taskPath); + +tmr.setDebugState(false); +tmr.setInput('command', 'custom'); +tmr.setInput('workingDirectory', ''); +tmr.setInput('customCommand', '-v'); +tmr.setExecResponse('npm -v', { + code: 0, + stdout: '4.6.1' +} as TaskLibAnswerExecResult); +tmr.run(); diff --git a/Tasks/Npm/Tests/custom-version.ts b/Tasks/Npm/Tests/custom-version.ts new file mode 100644 index 000000000000..afc305013ab1 --- /dev/null +++ b/Tasks/Npm/Tests/custom-version.ts @@ -0,0 +1,19 @@ +import * as path from 'path'; + +import { TaskLibAnswerExecResult } from 'vsts-task-lib/mock-answer'; +import * as tmrm from 'vsts-task-lib/mock-run'; + +import { NpmMockHelper } from './NpmMockHelper'; + +let taskPath = path.join(__dirname, '..', 'npm.js'); +let tmr = new NpmMockHelper(taskPath); + +tmr.setDebugState(true); +tmr.setInput('command', 'custom'); +tmr.setInput('workingDirectory', ''); +tmr.setInput('customCommand', '-v'); +tmr.setExecResponse('npm -v', { + code: 0, + stdout: '4.6.1' +} as TaskLibAnswerExecResult); +tmr.run(); diff --git a/Tasks/Npm/Tests/install-feed.ts b/Tasks/Npm/Tests/install-feed.ts new file mode 100644 index 000000000000..e55bbf339306 --- /dev/null +++ b/Tasks/Npm/Tests/install-feed.ts @@ -0,0 +1,22 @@ +import * as path from 'path'; + +import { TaskLibAnswerExecResult } from 'vsts-task-lib/mock-answer'; +import * as tmrm from 'vsts-task-lib/mock-run'; + +import { NpmCommand, NpmTaskInput, RegistryLocation } from '../constants'; +import { NpmMockHelper } from './NpmMockHelper'; + +let taskPath = path.join(__dirname, '..', 'npm.js'); +let tmr = new NpmMockHelper(taskPath); + +tmr.setInput(NpmTaskInput.Command, NpmCommand.Install); +tmr.setInput(NpmTaskInput.WorkingDir, ''); +tmr.setInput(NpmTaskInput.CustomRegistry, RegistryLocation.Feed); +tmr.setInput(NpmTaskInput.CustomFeed, 'SomeFeedId'); +tmr.setExecResponse('npm install', { + code: 0, + stdout: 'npm install successful' +} as TaskLibAnswerExecResult); +tmr.RegisterLocationServiceMocks(); + +tmr.run(); diff --git a/Tasks/Npm/Tests/install-npmFailure.ts b/Tasks/Npm/Tests/install-npmFailure.ts new file mode 100644 index 000000000000..1e817373a244 --- /dev/null +++ b/Tasks/Npm/Tests/install-npmFailure.ts @@ -0,0 +1,17 @@ +import * as path from 'path'; + +import { TaskLibAnswerExecResult } from 'vsts-task-lib/mock-answer'; +import * as tmrm from 'vsts-task-lib/mock-run'; + +import { NpmMockHelper } from './NpmMockHelper'; + +let taskPath = path.join(__dirname, '..', 'npm.js'); +let tmr = new NpmMockHelper(taskPath); + +tmr.setInput('command', 'install'); +tmr.setInput('workingDirectory', ''); +tmr.setExecResponse('npm install', { + code: -1, + stdout: 'some npm failure' +} as TaskLibAnswerExecResult); +tmr.run(); diff --git a/Tasks/Npm/Tests/install-npmrc.ts b/Tasks/Npm/Tests/install-npmrc.ts new file mode 100644 index 000000000000..6726fff22295 --- /dev/null +++ b/Tasks/Npm/Tests/install-npmrc.ts @@ -0,0 +1,20 @@ +import * as path from 'path'; + +import { TaskLibAnswerExecResult } from 'vsts-task-lib/mock-answer'; +import * as tmrm from 'vsts-task-lib/mock-run'; + +import { NpmCommand, NpmTaskInput, RegistryLocation } from '../constants'; +import { NpmMockHelper } from './NpmMockHelper'; + +let taskPath = path.join(__dirname, '..', 'npm.js'); +let tmr = new NpmMockHelper(taskPath); + +tmr.setInput(NpmTaskInput.Command, NpmCommand.Install); +tmr.setInput(NpmTaskInput.WorkingDir, ''); +tmr.setInput(NpmTaskInput.CustomRegistry, RegistryLocation.Npmrc); +tmr.setExecResponse('npm install', { + code: 0, + stdout: 'npm install successful' +} as TaskLibAnswerExecResult); + +tmr.run(); diff --git a/Tasks/Npm/Tests/publish-external.ts b/Tasks/Npm/Tests/publish-external.ts new file mode 100644 index 000000000000..3d62703a9409 --- /dev/null +++ b/Tasks/Npm/Tests/publish-external.ts @@ -0,0 +1,30 @@ +import * as path from 'path'; + +import { TaskLibAnswerExecResult } from 'vsts-task-lib/mock-answer'; +import * as tmrm from 'vsts-task-lib/mock-run'; + +import { NpmCommand, NpmTaskInput, RegistryLocation } from '../constants'; +import { NpmMockHelper } from './NpmMockHelper'; + +let taskPath = path.join(__dirname, '..', 'npm.js'); +let tmr = new NpmMockHelper(taskPath); + +tmr.setInput(NpmTaskInput.Command, NpmCommand.Publish); +tmr.setInput(NpmTaskInput.WorkingDir, 'workingDir'); +tmr.setInput(NpmTaskInput.PublishRegistry, RegistryLocation.External); +tmr.setInput(NpmTaskInput.PublishEndpoint, 'SomeEndpointId'); +let auth = { + scheme: 'Token', + parameters: { + 'apitoken': 'AUTHTOKEN' + } +}; +tmr.mockServiceEndpoint('SomeEndpointId', 'http://url', auth); +tmr.setExecResponse('npm publish', { + code: 0, + stdout: 'npm publish successful' +} as TaskLibAnswerExecResult); +tmr.answers.rmRF['workingDir\\.npmrc'] = { success: true }; +tmr.RegisterLocationServiceMocks(); + +tmr.run(); diff --git a/Tasks/Npm/Tests/publish-feed.ts b/Tasks/Npm/Tests/publish-feed.ts new file mode 100644 index 000000000000..5ddcdc6bea3f --- /dev/null +++ b/Tasks/Npm/Tests/publish-feed.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; + +import { TaskLibAnswerExecResult } from 'vsts-task-lib/mock-answer'; +import * as tmrm from 'vsts-task-lib/mock-run'; + +import { NpmCommand, NpmTaskInput, RegistryLocation } from '../constants'; +import { NpmMockHelper } from './NpmMockHelper'; + +let taskPath = path.join(__dirname, '..', 'npm.js'); +let tmr = new NpmMockHelper(taskPath); + +tmr.setInput(NpmTaskInput.Command, NpmCommand.Publish); +tmr.setInput(NpmTaskInput.WorkingDir, 'workingDir'); +tmr.setInput(NpmTaskInput.PublishRegistry, RegistryLocation.Feed); +tmr.setInput(NpmTaskInput.PublishFeed, 'SomeFeedId'); +tmr.setExecResponse('npm publish', { + code: 0, + stdout: 'npm publish successful' +} as TaskLibAnswerExecResult); +tmr.answers.rmRF['workingDir\\.npmrc'] = { success: true }; +tmr.RegisterLocationServiceMocks(); + +tmr.run(); diff --git a/Tasks/Npm/Tests/test-commandContainsSpaces-deprecated.ts b/Tasks/Npm/Tests/test-commandContainsSpaces-deprecated.ts deleted file mode 100644 index e2d0a503b863..000000000000 --- a/Tasks/Npm/Tests/test-commandContainsSpaces-deprecated.ts +++ /dev/null @@ -1,12 +0,0 @@ -import ma = require('vsts-task-lib/mock-answer'); -import tmrm = require('vsts-task-lib/mock-run'); -import path = require('path'); -import util = require('./NpmMockHelper'); - -let taskPath = path.join(__dirname, '..', 'npmtask.js'); -let taskMockRunner = new tmrm.TaskMockRunner(taskPath); -let npmMockHelper = new util.NpmMockHelper(taskMockRunner, "config get", "registry"); - -npmMockHelper.useDeprecatedTask(); - -npmMockHelper.run(); diff --git a/Tasks/Npm/Tests/test-commandContainsSpaces.ts b/Tasks/Npm/Tests/test-commandContainsSpaces.ts deleted file mode 100644 index fc1e45d5f5e8..000000000000 --- a/Tasks/Npm/Tests/test-commandContainsSpaces.ts +++ /dev/null @@ -1,10 +0,0 @@ -import ma = require('vsts-task-lib/mock-answer'); -import tmrm = require('vsts-task-lib/mock-run'); -import path = require('path'); -import util = require('./NpmMockHelper'); - -let taskPath = path.join(__dirname, '..', 'npmtask.js'); -let taskMockRunner = new tmrm.TaskMockRunner(taskPath); -let npmMockHelper = new util.NpmMockHelper(taskMockRunner, "config get", "registry"); - -npmMockHelper.run(); diff --git a/Tasks/Npm/Tests/test-commandWithoutArguments-deprecated.ts b/Tasks/Npm/Tests/test-commandWithoutArguments-deprecated.ts deleted file mode 100644 index e46a5d791f8e..000000000000 --- a/Tasks/Npm/Tests/test-commandWithoutArguments-deprecated.ts +++ /dev/null @@ -1,30 +0,0 @@ -import ma = require('vsts-task-lib/mock-answer'); -import tmrm = require('vsts-task-lib/mock-run'); -import path = require('path'); -import util = require('./NpmMockHelper'); - -let taskPath = path.join(__dirname, '..', 'npmtask.js'); -let taskMockRunner = new tmrm.TaskMockRunner(taskPath); -let npmMockHelper = new util.NpmMockHelper(taskMockRunner, "root", ""); - -if (process.argv.length == 3) { - if (process.argv[2] === "useDeprecated") { - npmMockHelper.useDeprecatedTask(); - } -} - -npmMockHelper.setDebugState(true); -npmMockHelper.mockAuthHelper(); -npmMockHelper.mockNpmConfigList(); - -npmMockHelper.useDeprecatedTask(); - -let root = path.join(process.env['INPUT_CWD'], "node_modules"); - -var execResult: ma.TaskLibAnswerExecResult = { - code: 0, - stdout: root, - stderr: "" -}; - -npmMockHelper.run(execResult); diff --git a/Tasks/Npm/Tests/test-commandWithoutArguments.ts b/Tasks/Npm/Tests/test-commandWithoutArguments.ts deleted file mode 100644 index 7328684da6b3..000000000000 --- a/Tasks/Npm/Tests/test-commandWithoutArguments.ts +++ /dev/null @@ -1,29 +0,0 @@ -import ma = require('vsts-task-lib/mock-answer'); -import tmrm = require('vsts-task-lib/mock-run'); -import path = require('path'); -import util = require('./NpmMockHelper'); - -let taskPath = path.join(__dirname, '..', 'npmtask.js'); -let taskMockRunner = new tmrm.TaskMockRunner(taskPath); -let npmMockHelper = new util.NpmMockHelper(taskMockRunner, "root", ""); -process.env['USERPROFILE'] = 'C:\\Users\\none'; - -if (process.argv.length == 3) { - if (process.argv[2] === "useDeprecated") { - npmMockHelper.useDeprecatedTask(); - } -} - -npmMockHelper.setDebugState(true); -npmMockHelper.mockAuthHelper(); -npmMockHelper.mockNpmConfigList(); - -let root = path.join(process.env['INPUT_CWD'], "node_modules"); - -var execResult: ma.TaskLibAnswerExecResult = { - code: 0, - stdout: root, - stderr: "" -}; - -npmMockHelper.run(execResult); diff --git a/Tasks/Npm/Tests/test-configlist-deprecated.ts b/Tasks/Npm/Tests/test-configlist-deprecated.ts deleted file mode 100644 index eec6bf0aeab5..000000000000 --- a/Tasks/Npm/Tests/test-configlist-deprecated.ts +++ /dev/null @@ -1,28 +0,0 @@ -import ma = require('vsts-task-lib/mock-answer'); -import tmrm = require('vsts-task-lib/mock-run'); -import path = require('path'); -import util = require('./NpmMockHelper'); - -let taskPath = path.join(__dirname, '..', 'npmtask.js'); -let taskMockRunner = new tmrm.TaskMockRunner(taskPath); -let npmMockHelper = new util.NpmMockHelper(taskMockRunner, "config", "list"); - -if (process.argv.length == 3) { - if (process.argv[2] === "useDeprecated") { - npmMockHelper.useDeprecatedTask(); - } -} - -npmMockHelper.setDebugState(true); -npmMockHelper.mockAuthHelper(); -npmMockHelper.mockNpmConfigList(); - -npmMockHelper.useDeprecatedTask(); - -var execResult: ma.TaskLibAnswerExecResult = { - code: 0, - stdout: "; cli configs", - stderr: "" -}; - -npmMockHelper.run(execResult); diff --git a/Tasks/Npm/Tests/test-configlist.ts b/Tasks/Npm/Tests/test-configlist.ts deleted file mode 100644 index 58177a4370f6..000000000000 --- a/Tasks/Npm/Tests/test-configlist.ts +++ /dev/null @@ -1,27 +0,0 @@ -import ma = require('vsts-task-lib/mock-answer'); -import tmrm = require('vsts-task-lib/mock-run'); -import path = require('path'); -import util = require('./NpmMockHelper'); - -let taskPath = path.join(__dirname, '..', 'npmtask.js'); -let taskMockRunner = new tmrm.TaskMockRunner(taskPath); -let npmMockHelper = new util.NpmMockHelper(taskMockRunner, "config", "list"); -process.env['USERPROFILE'] = 'C:\\Users\\none'; - -if (process.argv.length == 3) { - if (process.argv[2] === "useDeprecated") { - npmMockHelper.useDeprecatedTask(); - } -} - -npmMockHelper.setDebugState(true); -npmMockHelper.mockAuthHelper(); -npmMockHelper.mockNpmConfigList(); - -var execResult: ma.TaskLibAnswerExecResult = { - code: 0, - stdout: "; cli configs", - stderr: "" -}; - -npmMockHelper.run(execResult); diff --git a/Tasks/Npm/Tests/test-npmFailure-deprecated.ts b/Tasks/Npm/Tests/test-npmFailure-deprecated.ts deleted file mode 100644 index 6787eeb52491..000000000000 --- a/Tasks/Npm/Tests/test-npmFailure-deprecated.ts +++ /dev/null @@ -1,33 +0,0 @@ -import ma = require('vsts-task-lib/mock-answer'); -import tmrm = require('vsts-task-lib/mock-run'); -import path = require('path'); -import util = require('./NpmMockHelper'); - -let taskPath = path.join(__dirname, '..', 'npmtask.js'); -let taskMockRunner = new tmrm.TaskMockRunner(taskPath); -let npmMockHelper = new util.NpmMockHelper(taskMockRunner, "root", ""); - -let mock = require('vsts-task-lib/mock-toolrunner'); -mock.exec = () => { - throw "tool failure"; -}; - -if (process.argv.length == 3) { - if (process.argv[2] === "useDeprecated") { - npmMockHelper.useDeprecatedTask(); - } -} - -npmMockHelper.setDebugState(true); -npmMockHelper.mockAuthHelper(); -npmMockHelper.mockNpmConfigList(); - -npmMockHelper.useDeprecatedTask(); - -var execResult: ma.TaskLibAnswerExecResult = { - code: 1, - stdout: "", - stderr: "some error" -}; - -npmMockHelper.run(execResult); diff --git a/Tasks/Npm/Tests/test-npmFailure.ts b/Tasks/Npm/Tests/test-npmFailure.ts deleted file mode 100644 index 014805408c8b..000000000000 --- a/Tasks/Npm/Tests/test-npmFailure.ts +++ /dev/null @@ -1,32 +0,0 @@ -import ma = require('vsts-task-lib/mock-answer'); -import tmrm = require('vsts-task-lib/mock-run'); -import path = require('path'); -import util = require('./NpmMockHelper'); - -let taskPath = path.join(__dirname, '..', 'npmtask.js'); -let taskMockRunner = new tmrm.TaskMockRunner(taskPath); -let npmMockHelper = new util.NpmMockHelper(taskMockRunner, "root", ""); -process.env['USERPROFILE'] = 'C:\\Users\\none'; - -let mock = require('vsts-task-lib/mock-toolrunner'); -mock.exec = () => { - throw "tool failure"; -}; - -if (process.argv.length == 3) { - if (process.argv[2] === "useDeprecated") { - npmMockHelper.useDeprecatedTask(); - } -} - -npmMockHelper.setDebugState(true); -npmMockHelper.mockAuthHelper(); -npmMockHelper.mockNpmConfigList(); - -var execResult: ma.TaskLibAnswerExecResult = { - code: 1, - stdout: "", - stderr: "some error" -}; - -npmMockHelper.run(execResult); diff --git a/Tasks/Npm/constants.ts b/Tasks/Npm/constants.ts new file mode 100644 index 000000000000..9a1f579f1c55 --- /dev/null +++ b/Tasks/Npm/constants.ts @@ -0,0 +1,23 @@ +export class NpmCommand { + public static Install: string = 'install'; + public static Publish: string = 'publish'; + public static Custom: string = 'custom'; +} + +export class RegistryLocation { + public static Npmrc: string = 'useNpmrc'; + public static Feed: string = 'useFeed'; + public static External: string = 'useExternalRegistry'; +} + +export class NpmTaskInput { + public static Command: string = 'command'; + public static WorkingDir: string = 'workingDir'; + public static CustomCommand: string = 'customCommand'; + public static CustomRegistry: string = 'customRegistry'; + public static CustomFeed: string = 'customFeed'; + public static CustomEndpoint: string = 'customEndpoint'; + public static PublishRegistry: string = 'publishRegistry'; + public static PublishFeed: string = 'publishFeed'; + public static PublishEndpoint: string = 'publishEndpoint'; +} diff --git a/Tasks/Npm/make.json b/Tasks/Npm/make.json index 19039b24947a..2c63c0851048 100644 --- a/Tasks/Npm/make.json +++ b/Tasks/Npm/make.json @@ -1,14 +1,2 @@ { - "externals": { - "archivePackages": [ - { - "url": "https://vstsagenttools.blob.core.windows.net/tools/vsts-npm-auth/0.23.0/vsts-npm-auth.zip", - "dest": "./Npm/vsts-npm-auth/" - }, - { - "url": "https://vstsagenttools.blob.core.windows.net/tools/CredentialProvider.TeamBuild/15.105.25712.0/CredentialProvider.TeamBuild.zip", - "dest": "./Npm/CredentialProvider/" - } - ] - } } diff --git a/Tasks/Npm/npm.ts b/Tasks/Npm/npm.ts new file mode 100644 index 000000000000..483f229931ec --- /dev/null +++ b/Tasks/Npm/npm.ts @@ -0,0 +1,37 @@ +import * as path from 'path'; +import * as url from 'url'; + +import * as tl from 'vsts-task-lib/task'; +import * as vsts from 'vso-node-api/WebApi'; +import * as Q from 'q'; + +import { NpmCommand, NpmTaskInput, RegistryLocation } from './constants'; +import * as npmCustom from './npmcustom'; +import * as npmInstall from './npminstall'; +import * as npmPublish from './npmpublish'; +import { GetRegistries, NormalizeRegistry } from './npmrcparser'; +import { INpmRegistry, NpmRegistry } from './npmregistry'; +import { NpmToolRunner } from './npmtoolrunner'; +import * as util from './util'; + +async function main(): Promise { + tl.setResourcePath(path.join(__dirname, 'task.json')); + + const command = tl.getInput(NpmTaskInput.Command); + switch (command) { + case NpmCommand.Install: + return npmInstall.run(); + case NpmCommand.Publish: + return npmPublish.run(); + case NpmCommand.Custom: + return npmCustom.run(); + default: + tl.setResult(tl.TaskResult.Failed, tl.loc('UnknownCommand', command)); + return; + } +} + +main().catch(error => { + tl.rmRF(util.getTempPath()); + tl.setResult(tl.TaskResult.Failed, error); +}); diff --git a/Tasks/Npm/npmcustom.ts b/Tasks/Npm/npmcustom.ts new file mode 100644 index 000000000000..6eccfdce01e3 --- /dev/null +++ b/Tasks/Npm/npmcustom.ts @@ -0,0 +1,48 @@ +import * as path from 'path'; + +import * as tl from 'vsts-task-lib/task'; + +import { NpmCommand, NpmTaskInput, RegistryLocation } from './constants'; +import { INpmRegistry, NpmRegistry } from './npmregistry'; +import { NpmToolRunner } from './npmtoolrunner'; +import * as util from './util'; + +export async function run(command?: string): Promise { + let workingDir = tl.getInput(NpmTaskInput.WorkingDir) || process.cwd(); + let npmrc = util.getTempNpmrcPath(); + let npmRegistries: INpmRegistry[] = await util.getLocalNpmRegistries(workingDir); + + let registryLocation = tl.getInput(NpmTaskInput.CustomRegistry); + switch (registryLocation) { + case RegistryLocation.Feed: + tl.debug(tl.loc('UseFeed')); + let feedId = tl.getInput(NpmTaskInput.CustomFeed, true); + npmRegistries.push(await NpmRegistry.FromFeedId(feedId)); + break; + case RegistryLocation.Npmrc: + tl.debug(tl.loc('UseNpmrc')); + let endpointId = tl.getInput(NpmTaskInput.CustomEndpoint); + if (endpointId) { + npmRegistries.push(NpmRegistry.FromServiceEndpoint(endpointId, true)); + } + break; + } + + for (let registry of npmRegistries) { + if (registry.authOnly === false) { + tl.debug(tl.loc('UsingRegistry', registry.url)); + util.appendToNpmrc(npmrc, `registry=${registry.url}\n`); + } + + tl.debug(tl.loc('AddingAuthRegistry', registry.url)); + util.appendToNpmrc(npmrc, `${registry.auth}\n`); + } + + let npm = new NpmToolRunner(workingDir, npmrc); + npm.line(command || tl.getInput(NpmTaskInput.CustomCommand, true)); + + await npm.exec(); + + tl.rmRF(npmrc); + tl.rmRF(util.getTempPath()); +} diff --git a/Tasks/Npm/npminstall.ts b/Tasks/Npm/npminstall.ts new file mode 100644 index 000000000000..92778a7285d5 --- /dev/null +++ b/Tasks/Npm/npminstall.ts @@ -0,0 +1,9 @@ +import * as path from 'path'; + +import * as tl from 'vsts-task-lib/task'; + +import * as npmCustom from './npmcustom'; + +export async function run(): Promise { + return npmCustom.run('install'); +} diff --git a/Tasks/Npm/npmpublish.ts b/Tasks/Npm/npmpublish.ts new file mode 100644 index 000000000000..e6de31a4d144 --- /dev/null +++ b/Tasks/Npm/npmpublish.ts @@ -0,0 +1,44 @@ +import * as tl from 'vsts-task-lib/task'; + +import { NpmTaskInput, RegistryLocation } from './constants'; +import { INpmRegistry, NpmRegistry } from './npmregistry'; +import { NpmToolRunner } from './npmtoolrunner'; +import * as util from './util'; + +export async function run(command?: string): Promise { + let workingDir = tl.getInput(NpmTaskInput.WorkingDir) || process.cwd(); + let npmrc = util.getTempNpmrcPath(); + let npmRegistry: INpmRegistry; + + let registryLocation = tl.getInput(NpmTaskInput.PublishRegistry); + switch (registryLocation) { + case RegistryLocation.Feed: + tl.debug(tl.loc('PublishFeed')); + let feedId = tl.getInput(NpmTaskInput.PublishFeed, true); + npmRegistry = await NpmRegistry.FromFeedId(feedId); + break; + case RegistryLocation.External: + tl.debug(tl.loc('PublishExternal')); + let endpointId = tl.getInput(NpmTaskInput.PublishEndpoint, true); + npmRegistry = NpmRegistry.FromServiceEndpoint(endpointId); + break; + } + + tl.debug(tl.loc('PublishRegistry', npmRegistry.url)); + util.appendToNpmrc(npmrc, `registry=${npmRegistry.url}\n`); + util.appendToNpmrc(npmrc, `${npmRegistry.auth}\n`); + + let npm = new NpmToolRunner(workingDir, npmrc); + npm.line('publish'); + + // We delete their project .npmrc so it won't override our user .npmrc + const projectNpmrc = `${workingDir}\\.npmrc`; + util.saveFile(projectNpmrc); + tl.rmRF(projectNpmrc); + + await npm.exec(); + util.restoreFile(projectNpmrc); + + tl.rmRF(npmrc); + tl.rmRF(util.getTempPath()); +} diff --git a/Tasks/Npm/npmrcparser.ts b/Tasks/Npm/npmrcparser.ts new file mode 100644 index 000000000000..ee17d806af39 --- /dev/null +++ b/Tasks/Npm/npmrcparser.ts @@ -0,0 +1,30 @@ +import * as fs from 'fs'; +import * as ini from 'ini'; +import * as url from 'url'; + +import * as tl from 'vsts-task-lib/task'; + +export function GetRegistries(npmrc: string): string[] { + let registries: string[] = []; + let config = ini.parse(fs.readFileSync(npmrc).toString()); + + for (let key in config) { + let colonIndex = key.indexOf(':'); + if (key.substring(colonIndex + 1).toLowerCase() === 'registry') { + config[key] = NormalizeRegistry(config[key]); + registries.push(config[key]); + } + } + + // save the .npmrc with normalized registries + tl.writeFile(npmrc, ini.stringify(config)); + return registries; +} + +export function NormalizeRegistry(registry: string): string { + if (registry) { + registry = registry.slice(-1) !== '/' ? registry + '/' : registry; + } + + return registry; +} diff --git a/Tasks/Npm/npmregistry.ts b/Tasks/Npm/npmregistry.ts new file mode 100644 index 000000000000..e94ac56cbbf2 --- /dev/null +++ b/Tasks/Npm/npmregistry.ts @@ -0,0 +1,72 @@ +import * as url from 'url'; + +import * as tl from 'vsts-task-lib/task'; + +import { NormalizeRegistry } from './npmrcparser'; +import * as util from './util'; + +export interface INpmRegistry { + url: string; + auth: string; + authOnly: boolean; +} + +export class NpmRegistry implements INpmRegistry { + public url: string; + public auth: string; + public authOnly: boolean; + + constructor(url: string, auth: string, authOnly?: boolean) { + this.url = url; + this.auth = auth; + this.authOnly = authOnly || false; + } + + public static FromServiceEndpoint(endpointId: string, authOnly?: boolean): NpmRegistry { + let email: string; + let username: string; + let password: string; + let endpointAuth: tl.EndpointAuthorization; + let url: string; + try { + endpointAuth = tl.getEndpointAuthorization(endpointId, false); + } catch (exception) { + throw new Error(tl.loc('ServiceEndpointNotDefined')); + } + + try { + url = NormalizeRegistry(tl.getEndpointUrl(endpointId, false)); + } catch (exception) { + throw new Error(tl.loc('ServiceEndpointUrlNotDefined')); + } + + switch (endpointAuth.scheme) { + case 'UsernamePassword': + username = endpointAuth.parameters['username']; + password = endpointAuth.parameters['password']; + email = username; // npm needs an email to be set in order to publish, this is ignored on npmjs + break; + case 'Token': + email = 'VssEmail'; + username = 'VssToken'; + password = endpointAuth.parameters['apitoken']; + break; + } + + let nerfed = util.toNerfDart(url); + let auth = `${nerfed}:username=${username} + ${nerfed}:_password=${new Buffer(password).toString('base64')} + ${nerfed}:email=${email} + ${nerfed}:always-auth=true`; + + return new NpmRegistry(url, auth, authOnly); + } + + public static async FromFeedId(feedId: string, authOnly?: boolean): Promise { + let url = NormalizeRegistry(await util.getFeedRegistryUrl(feedId)); + let nerfed = util.toNerfDart(url); + let auth = `${nerfed}:_authToken=${util.getSystemAccessToken()}`; + + return new NpmRegistry(url, auth, authOnly); + } +} diff --git a/Tasks/Npm/npmtask.ts b/Tasks/Npm/npmtask.ts deleted file mode 100644 index 4d9a733e3a7f..000000000000 --- a/Tasks/Npm/npmtask.ts +++ /dev/null @@ -1,248 +0,0 @@ -import Q = require('q'); -import fs = require('fs'); -import path = require('path'); -import url = require('url'); -import tl = require('vsts-task-lib/task'); -import trm = require('vsts-task-lib/toolrunner'); -var extend = require('util')._extend; - -interface EnvironmentDictionary { [key: string]: string; } - -tl.setResourcePath(path.join( __dirname, 'task.json')); - -executeTask(); - -async function executeTask() { - - var cwd = tl.getPathInput('cwd', true, false); - tl.mkdirP(cwd); - tl.cd(cwd); - - var command = tl.getInput('command', true); - if (command.indexOf(' ') >= 0) { - tl.setResult(tl.TaskResult.Failed, tl.loc('InvalidCommand')); - return; - } - - var npmRunner = tl.tool(tl.which('npm', true)); - npmRunner.arg(command); - npmRunner.line(tl.getInput('arguments', false)); - - - if(shouldUseDeprecatedTask()) { - - // deprecated version of task, which just runs the npm command with NO auth support. - try{ - var code : number = await npmRunner.exec(); - tl.setResult(code, tl.loc('NpmReturnCode', code)); - } catch (err) { - tl.debug('taskRunner fail'); - tl.setResult(tl.TaskResult.Failed, tl.loc('NpmFailed', err.message)); - } - } else { - - // new task version with auth support - try{ - - var npmrcPath: string = path.join(cwd, '.npmrc'); - var tempNpmrcPath : string = getTempNpmrcPath(); - - var debugLog: boolean = tl.getVariable('system.debug') && tl.getVariable('system.debug').toLowerCase() === 'true'; - - var shouldRunAuthHelper: boolean = tl.osType().toLowerCase() === 'windows_nt' && tl.exist(npmrcPath); - if(shouldRunAuthHelper) { - copyUserNpmrc(tempNpmrcPath); - await runNpmAuthHelperAsync(getNpmAuthHelperRunner(npmrcPath, tempNpmrcPath, debugLog)); - } - - // set required environment variables for npm execution - var npmExecOptions = { - env: extend({}, process.env) - }; - - if(shouldRunAuthHelper){ - npmExecOptions.env['npm_config_userconfig'] = tempNpmrcPath; - } - - if(debugLog) { - npmExecOptions.env['npm_config_loglevel'] = 'verbose'; - } - - await tryRunNpmConfigAsync(getNpmConfigRunner(debugLog), npmExecOptions); - var code : number = await runNpmCommandAsync(npmRunner, npmExecOptions); - cleanUpTempNpmrcPath(tempNpmrcPath); - tl.setResult(code, tl.loc('NpmReturnCode', code)); - } catch (err) { - cleanUpTempNpmrcPath(tempNpmrcPath); - tl.setResult(tl.TaskResult.Failed, tl.loc('NpmFailed', err.message)); - } - } -} - -async function runNpmAuthHelperAsync(npmAuthRunner: trm.ToolRunner) : Promise { - try{ - var execOptions = { - env: extend({}, process.env) - }; - execOptions.env = addBuildCredProviderEnv(execOptions.env); - - var code : number = await npmAuthRunner.exec(execOptions); - tl.debug('Auth helper exitted with code: ' + code); - return Q(code); - } catch (err) { - // warn on any auth failure and try to run the task. - tl.warning(tl.loc('NpmAuthFailed', err.message)); - } -} - -function copyUserNpmrc(tempNpmrcPath: string) { - // copy the user level npmrc contents, if it exists. - var currentUserNpmrcPath : string = getUserNpmrcPath(); - if(tl.exist(currentUserNpmrcPath)) { - tl.debug(`Copying ${currentUserNpmrcPath} to ${tempNpmrcPath} ...`); - tl.cp(currentUserNpmrcPath, tempNpmrcPath, /* options */ null, /* continueOnError */ true); - } -} - -function getUserNpmrcPath() { - var userNpmRc = process.env['npm_config_userconfig']; - if(!userNpmRc){ - // default npm rc is located at user's home folder. - userNpmRc = path.join(process.env['USERPROFILE'], '.npmrc'); - } - tl.debug(`User npm rc: ${userNpmRc}`); - return userNpmRc; -} - -async function tryRunNpmConfigAsync(npmConfigRunner: trm.ToolRunner, execOptions : trm.IExecOptions) { - try { - var code = await npmConfigRunner.exec(execOptions); - } catch (err) { - // 'npm config list' comamnd is run only for debugging/diagnostic - // purposes only. Failure of this shouldn't be fatal. - tl.warning(tl.loc('NpmConfigFailed', err.Message)); - } -} - -async function runNpmCommandAsync(npmCommandRunner: trm.ToolRunner, execOptions : trm.IExecOptions) : Promise { - try{ - var code: number = await npmCommandRunner.exec(execOptions); - tl.debug('Npm command succeeded with code: ' + code); - return Q(code); - } catch (err) { - tl.debug(tl.loc('NpmFailed', err.message)); - throw err; - } -} - -function shouldUseDeprecatedTask() : boolean { - return tl.getVariable('USE_DEPRECATED_TASK_VERSION') && tl.getVariable('USE_DEPRECATED_TASK_VERSION').toLowerCase() === 'true'; -} - -function getNpmAuthHelperRunner(npmrcPath: string, tempUserNpmrcPath: string, includeDebugLogs: boolean): trm.ToolRunner { - let npmAuthHelper = tl.tool(getNpmAuthHelperPath()); - let verbosityString = includeDebugLogs ? 'Detailed' : 'Normal'; - npmAuthHelper.line(`-NonInteractive -Verbosity ${verbosityString} -Config "${npmrcPath}" -TargetConfig "${tempUserNpmrcPath}"`); - return npmAuthHelper; -} - -function getNpmAuthHelperPath(): string { - let authHelperExternalPath = path.join(__dirname, 'Npm', 'vsts-npm-auth'); - let allFiles = tl.find(authHelperExternalPath); - var matchingFiles = allFiles.filter(tl.filter('vsts-npm-auth.exe', {nocase: true, matchBase: true})); - - if (matchingFiles.length !== 1) { - // Let the framework produce the desired error message - tl.checkPath(path.join(authHelperExternalPath, "bin", "vsts-npm-auth.exe"), 'vsts-npm-auth'); - } - - return matchingFiles[0]; -} - -function getNpmConfigRunner(includeDebugLogs: boolean): trm.ToolRunner { - var npmConfig = tl.tool(tl.which('npm', true)); - npmConfig.line('config list'); - if(includeDebugLogs) { - npmConfig.arg('-l'); - } - return npmConfig; -} - -function getTempNpmrcPath() : string { - var tempNpmrcDir - = tl.getVariable('Agent.BuildDirectory') - || tl.getVariable('Agent.ReleaseDirectory') - || process.cwd(); - tempNpmrcDir = path.join(tempNpmrcDir, 'npm'); - tl.mkdirP(tempNpmrcDir); - var tempUserNpmrcPath: string = path.join(tempNpmrcDir, 'auth.' + tl.getVariable('build.buildId') + '.npmrc'); - return tempUserNpmrcPath; -} - -function cleanUpTempNpmrcPath(tempUserNpmrcPath: string) { - tl.debug('cleaning up...') - if(tl.exist(tempUserNpmrcPath)) { - tl.debug(`deleting temporary npmrc at '${tempUserNpmrcPath}'` ); - tl.rmRF(tempUserNpmrcPath, /* continueOnError */ false); - } -} - -function addBuildCredProviderEnv(env: EnvironmentDictionary) : EnvironmentDictionary { - - var credProviderPath : string = path.join(__dirname, 'Npm/CredentialProvider'); - - // get build access token - var accessToken : string = getSystemAccessToken(); - - // get uri prefixes - var serviceUri : string = tl.getEndpointUrl('SYSTEMVSSCONNECTION', false); - var urlPrefixes : string[] = assumeNpmUriPrefixes(serviceUri); - tl.debug(`discovered URL prefixes: ${urlPrefixes}`); - - // Note to readers: This variable will be going away once we have a fix for the location service for - // customers behind proxies - let testPrefixes = tl.getVariable("NpmTasks.ExtraUrlPrefixesForTesting"); - if (testPrefixes) { - urlPrefixes = urlPrefixes.concat(testPrefixes.split(";")); - tl.debug(`all URL prefixes: ${urlPrefixes}`); - } - - // These env variables are using NUGET because the credential provider that is being used - // was built only when NuGet was supported. It is basically using environment variable to - // pull out the access token, hence can be used in Npm scenario as well. - env['VSS_NUGET_ACCESSTOKEN'] = accessToken; - env['VSS_NUGET_URI_PREFIXES'] = urlPrefixes.join(';'); - env['NPM_CREDENTIALPROVIDERS_PATH'] = credProviderPath; - env['VSS_DISABLE_DEFAULTCREDENTIALPROVIDER'] = '1'; - return env; -} - -// Below logic exists in nuget common module as well, but due to tooling issue -// where two tasks which use different tasks lib versions can't use the same common -// module, it's being duplicated here. -function getSystemAccessToken(): string { - tl.debug('Getting credentials for local feeds'); - var auth = tl.getEndpointAuthorization('SYSTEMVSSCONNECTION', false); - if (auth.scheme === 'OAuth') { - tl.debug('Got auth token'); - return auth.parameters['AccessToken']; - } else { - tl.warning(tl.loc('BuildCredentialsWarn')); - } -} - -function assumeNpmUriPrefixes(collectionUri: string): string[] { - var prefixes = [collectionUri]; - - var collectionUrlObject = url.parse(collectionUri); - if(collectionUrlObject.hostname.toUpperCase().endsWith('.VISUALSTUDIO.COM')) { - var hostparts = collectionUrlObject.hostname.split('.'); - var packagingHostName = hostparts[0] + '.pkgs.visualstudio.com'; - collectionUrlObject.hostname = packagingHostName; - // remove the host property so it doesn't override the hostname property for url.format - delete collectionUrlObject.host; - prefixes.push(url.format(collectionUrlObject)); - } - - return prefixes; -} \ No newline at end of file diff --git a/Tasks/Npm/npmtoolrunner.ts b/Tasks/Npm/npmtoolrunner.ts new file mode 100644 index 000000000000..da356309a099 --- /dev/null +++ b/Tasks/Npm/npmtoolrunner.ts @@ -0,0 +1,77 @@ +import * as fs from 'fs'; +import { format, parse, Url } from 'url'; + +import * as tl from 'vsts-task-lib/task'; +import * as tr from 'vsts-task-lib/toolrunner'; + +export class NpmToolRunner extends tr.ToolRunner { + private dbg: boolean; + + constructor(private workingDirectory: string, private npmrc?: string) { + super('npm'); + + this.on('debug', (message: string) => { + tl.debug(message); + }); + + let debugVar = tl.getVariable('System.Debug') || ''; + if (debugVar.toLowerCase() === 'true') { + this.dbg = true; + } + } + + public exec(options?: tr.IExecOptions): Q.Promise { + options = this._prepareNpmEnvironment(options) as tr.IExecOptions; + + return super.exec(options); + } + + public execSync(options?: tr.IExecSyncOptions): tr.IExecSyncResult { + options = this._prepareNpmEnvironment(options); + + return super.execSync(options); + } + + private static _getProxyFromEnvironment(): string { + let proxyUrl: string = tl.getVariable('agent.proxyurl'); + if (proxyUrl) { + let proxy: Url = parse(proxyUrl); + let proxyUsername: string = tl.getVariable('agent.proxyusername') || ''; + let proxyPassword: string = tl.getVariable('agent.proxypassword') || ''; + + let auth = `${proxyUsername}:${proxyPassword}`; + proxy.auth = auth; + + return format(proxy); + } + + return undefined; + } + + private _prepareNpmEnvironment(options?: tr.IExecSyncOptions): tr.IExecSyncOptions { + options = options || {}; + options.cwd = this.workingDirectory; + + if (options.env === undefined) { + options.env = process.env; + } + + if (this.dbg) { + options.env['NPM_CONFIG_LOGLEVEL'] = 'verbose'; + } + + if (this.npmrc) { + options.env['NPM_CONFIG_USERCONFIG'] = this.npmrc; + } + + let proxy = NpmToolRunner._getProxyFromEnvironment(); + if (proxy) { + tl.debug(`Using proxy "${proxy}" for npm`); + options.env['NPM_CONFIG_PROXY'] = proxy; + options.env['NPM_CONFIG_HTTPS-PROXY'] = proxy; + } + + let config = tl.execSync('npm', `config list ${this.dbg ? '-l' : ''}`, options); + return options; + } +} diff --git a/Tasks/Npm/package.json b/Tasks/Npm/package.json index 8455514d593e..c58831c2d02d 100644 --- a/Tasks/Npm/package.json +++ b/Tasks/Npm/package.json @@ -17,6 +17,9 @@ }, "homepage": "https://github.com/microsoft/vsts-tasks#readme", "dependencies": { - "vsts-task-lib": "^0.9.20" + "ini": "^1.3.4", + "mockery": "^1.7.0", + "vso-node-api": "^6.2.2-preview", + "vsts-task-lib": "^2.0.5" } } diff --git a/Tasks/Npm/task.json b/Tasks/Npm/task.json index 49b4ee2bf465..76d6f8ab41bf 100644 --- a/Tasks/Npm/task.json +++ b/Tasks/Npm/task.json @@ -2,14 +2,14 @@ "id": "FE47E961-9FA8-4106-8639-368C022D43AD", "name": "Npm", "friendlyName": "npm", - "description": "Run an npm command", + "description": "Install and publish npm packages, or run an npm command. Supports npmjs.com and authenticated registries like Package Management.", "helpMarkDown": "[More Information](https://go.microsoft.com/fwlink/?LinkID=613746)", "category": "Package", "author": "Microsoft Corporation", "version": { - "Major": 0, - "Minor": 2, - "Patch": 22 + "Major": 1, + "Minor": 0, + "Patch": 0 }, "runsOn": [ "Agent", @@ -20,43 +20,146 @@ ], "minimumAgentVersion": "1.91.0", "instanceNameFormat": "npm $(command)", - "inputs": [ + "groups": [ { - "name": "cwd", - "type": "filePath", - "label": "working folder", - "defaultValue": "", - "required": false, - "helpMarkDown": "Working directory where the npm command is run. Defaults to the root of the repo." + "name": "customRegistries", + "displayName": "Custom registries and authentication", + "isExpanded": false, + "visibleRule": "command = install || command = custom" }, + { + "name": "publishRegistries", + "displayName": "Destination registry and authentication", + "isExpanded": true, + "visibleRule": "command = publish" + } + ], + "inputs": [ { "name": "command", + "label": "Command", + "helpMarkDown": "The command and arguments which will be passed to npm for execution.", + "type": "pickList", + "required": true, + "options": { + "install": "install", + "publish": "publish", + "custom": "custom" + }, + "defaultValue": "install" + }, + { + "name": "workingDir", + "label": "Working folder with package.json", + "helpMarkDown": "Path to the folder containing the target package.json and .npmrc files. Select the folder, not the file e.g. \"/packages/mypackage\".", + "type": "filePath" + }, + { + "name": "customCommand", + "label": "Command and arguments", + "helpMarkDown": "Custom command to run, e.g. \"dist-tag ls mypackage\".", "type": "string", - "label": "npm command", - "defaultValue": "install", + "visibleRule": "command = custom", "required": true }, { - "name": "arguments", - "type": "string", - "label": "arguments", - "defaultValue": "", - "helpMarkDown": "Additional arguments passed to npm.", - "required": false + "groupName": "customRegistries", + "name": "customRegistry", + "label": "Registries to use", + "helpMarkDown": "You can either commit a .npmrc file to your source code repository and set its path here or select a registry from VSTS here.", + "type": "radio", + "options": { + "useNpmrc": "Registries in my .npmrc", + "useFeed": "Registry I select here" + }, + "defaultValue": "useNpmrc" + }, + { + "groupName": "customRegistries", + "name": "customFeed", + "label": "Use packages from this VSTS/TFS registry", + "helpMarkDown": "Include the selected feed in the generated .npmrc.", + "type": "pickList", + "visibleRule": "customRegistry = useFeed", + "required": true + }, + { + "groupName": "customRegistries", + "name": "customEndpoint", + "label": "Credentials for registries outside this account/collection", + "helpMarkDown": "Credentials to use for external registries located in the project's .npmrc. For registries in this account/collection, leave this blank; the build’s credentials are used automatically.", + "type": "connectedService:externalnpmregistry", + "visibleRule": "customRegistry = useNpmrc" + }, + { + "groupName": "publishRegistries", + "name": "publishRegistry", + "label": "Registry location", + "helpMarkDown": "Registry the command will target.", + "type": "radio", + "options": { + "useExternalRegistry": "External npm registry (including other accounts/collections)", + "useFeed": "Registry I select here" + }, + "defaultValue": "useExternalRegistry" + }, + { + "groupName": "publishRegistries", + "name": "publishFeed", + "label": "Target registry", + "helpMarkDown": "Select a registry hosted in this account. You must have Package Management installed and licensed to select a registry here.", + "type": "pickList", + "visibleRule": "publishRegistry = useFeed", + "required": true + }, + { + "groupName": "publishRegistries", + "name": "publishEndpoint", + "label": "External Registry", + "helpMarkDown": "Credentials to use for publishing to an external registry.", + "type": "connectedService:externalnpmregistry", + "visibleRule": "publishRegistry = useExternalRegistry", + "required": true + } + ], + "dataSourceBindings": [ + { + "target": "customFeed", + "endpointId": "tfs:feed", + "endpointUrl": "{{endpoint.url}}/_apis/packaging/feeds", + "resultSelector": "jsonpath:$.value[*]", + "resultTemplate": "{ \"Value\": \"{{{id}}}\", \"DisplayValue\": \"{{{name}}}\"}" + }, + { + "target": "publishFeed", + "endpointId": "tfs:feed", + "endpointUrl": "{{endpoint.url}}/_apis/packaging/feeds", + "resultSelector": "jsonpath:$.value[*]", + "resultTemplate": "{ \"Value\": \"{{{id}}}\", \"DisplayValue\": \"{{{name}}}\"}" } ], "execution": { "Node": { - "target": "npmtask.js", - "argumentFormat": "" + "target": "npm.js" } }, "messages": { - "InvalidCommand": "Only one command should be used for npm. Use 'Arguments' input for additional arguments.", - "NpmReturnCode": "npm exited with return code: %d", - "NpmFailed": "npm failed with error: %s", - "NpmConfigFailed": "Failed to log the npm config information. Error : %s", - "NpmAuthFailed": "Failed to get the required authentication tokens for npm. Error: %s", - "BuildCredentialsWarn": "Could not determine credentials to use for npm" + "FoundBuildCredentials": "Found build credentials", + "NoBuildCredentials": "Could not find build credentials", + "UnknownCommand": "Unknown command: %s", + "MultipleProjectConfigs": "More than one project .npmrc found in $s", + "ServiceEndpointNotDefined": "Couldn't find Service Endpoint, make sure the selected endpoint still exists.", + "ServiceEndpointUrlNotDefined": "Couldn't find Url for Service Endpoint, make sure Service Endpoint is correctly configured.", + "SavingFile": "Saving file %s", + "RestoringFile": "Restoring file %s", + "PublishFeed": "Publishing to internal feed", + "PublishExternal": "Publishing to external registry", + "UseFeed": "Using internal feed", + "UseNpmrc": "Using registries in .npmrc", + "PublishRegistry": "Publishing to registry: %s", + "UsingRegistry": "Using registry: %s", + "AddingAuthRegistry": "Adding auth for registry: %s", + "FoundLocalRegistries": "Found %d registries in this account/collection", + "ForcePackagingUrl": "Packaging collection url forced to: %s" } -} \ No newline at end of file +} diff --git a/Tasks/Npm/task.loc.json b/Tasks/Npm/task.loc.json index 0485bfcfdb4e..2ddd66ff6689 100644 --- a/Tasks/Npm/task.loc.json +++ b/Tasks/Npm/task.loc.json @@ -7,9 +7,9 @@ "category": "Package", "author": "Microsoft Corporation", "version": { - "Major": 0, - "Minor": 2, - "Patch": 22 + "Major": 1, + "Minor": 0, + "Patch": 0 }, "runsOn": [ "Agent", @@ -20,43 +20,146 @@ ], "minimumAgentVersion": "1.91.0", "instanceNameFormat": "ms-resource:loc.instanceNameFormat", - "inputs": [ + "groups": [ { - "name": "cwd", - "type": "filePath", - "label": "ms-resource:loc.input.label.cwd", - "defaultValue": "", - "required": false, - "helpMarkDown": "ms-resource:loc.input.help.cwd" + "name": "customRegistries", + "displayName": "ms-resource:loc.group.displayName.customRegistries", + "isExpanded": false, + "visibleRule": "command = install || command = custom" }, + { + "name": "publishRegistries", + "displayName": "ms-resource:loc.group.displayName.publishRegistries", + "isExpanded": true, + "visibleRule": "command = publish" + } + ], + "inputs": [ { "name": "command", - "type": "string", "label": "ms-resource:loc.input.label.command", - "defaultValue": "install", - "required": true + "helpMarkDown": "ms-resource:loc.input.help.command", + "type": "pickList", + "required": true, + "options": { + "install": "install", + "publish": "publish", + "custom": "custom" + }, + "defaultValue": "install" }, { - "name": "arguments", + "name": "workingDir", + "label": "ms-resource:loc.input.label.workingDir", + "helpMarkDown": "ms-resource:loc.input.help.workingDir", + "type": "filePath" + }, + { + "name": "customCommand", + "label": "ms-resource:loc.input.label.customCommand", + "helpMarkDown": "ms-resource:loc.input.help.customCommand", "type": "string", - "label": "ms-resource:loc.input.label.arguments", - "defaultValue": "", - "helpMarkDown": "ms-resource:loc.input.help.arguments", - "required": false + "visibleRule": "command = custom", + "required": true + }, + { + "groupName": "customRegistries", + "name": "customRegistry", + "label": "ms-resource:loc.input.label.customRegistry", + "helpMarkDown": "ms-resource:loc.input.help.customRegistry", + "type": "radio", + "options": { + "useNpmrc": "Registries in my .npmrc", + "useFeed": "Registry I select here" + }, + "defaultValue": "useNpmrc" + }, + { + "groupName": "customRegistries", + "name": "customFeed", + "label": "ms-resource:loc.input.label.customFeed", + "helpMarkDown": "ms-resource:loc.input.help.customFeed", + "type": "pickList", + "visibleRule": "customRegistry = useFeed", + "required": true + }, + { + "groupName": "customRegistries", + "name": "customEndpoint", + "label": "ms-resource:loc.input.label.customEndpoint", + "helpMarkDown": "ms-resource:loc.input.help.customEndpoint", + "type": "connectedService:externalnpmregistry", + "visibleRule": "customRegistry = useNpmrc" + }, + { + "groupName": "publishRegistries", + "name": "publishRegistry", + "label": "ms-resource:loc.input.label.publishRegistry", + "helpMarkDown": "ms-resource:loc.input.help.publishRegistry", + "type": "radio", + "options": { + "useExternalRegistry": "External npm registry (including other accounts/collections)", + "useFeed": "Registry I select here" + }, + "defaultValue": "useExternalRegistry" + }, + { + "groupName": "publishRegistries", + "name": "publishFeed", + "label": "ms-resource:loc.input.label.publishFeed", + "helpMarkDown": "ms-resource:loc.input.help.publishFeed", + "type": "pickList", + "visibleRule": "publishRegistry = useFeed", + "required": true + }, + { + "groupName": "publishRegistries", + "name": "publishEndpoint", + "label": "ms-resource:loc.input.label.publishEndpoint", + "helpMarkDown": "ms-resource:loc.input.help.publishEndpoint", + "type": "connectedService:externalnpmregistry", + "visibleRule": "publishRegistry = useExternalRegistry", + "required": true + } + ], + "dataSourceBindings": [ + { + "target": "customFeed", + "endpointId": "tfs:feed", + "endpointUrl": "{{endpoint.url}}/_apis/packaging/feeds", + "resultSelector": "jsonpath:$.value[*]", + "resultTemplate": "{ \"Value\": \"{{{id}}}\", \"DisplayValue\": \"{{{name}}}\"}" + }, + { + "target": "publishFeed", + "endpointId": "tfs:feed", + "endpointUrl": "{{endpoint.url}}/_apis/packaging/feeds", + "resultSelector": "jsonpath:$.value[*]", + "resultTemplate": "{ \"Value\": \"{{{id}}}\", \"DisplayValue\": \"{{{name}}}\"}" } ], "execution": { "Node": { - "target": "npmtask.js", - "argumentFormat": "" + "target": "npm.js" } }, "messages": { - "InvalidCommand": "ms-resource:loc.messages.InvalidCommand", - "NpmReturnCode": "ms-resource:loc.messages.NpmReturnCode", - "NpmFailed": "ms-resource:loc.messages.NpmFailed", - "NpmConfigFailed": "ms-resource:loc.messages.NpmConfigFailed", - "NpmAuthFailed": "ms-resource:loc.messages.NpmAuthFailed", - "BuildCredentialsWarn": "ms-resource:loc.messages.BuildCredentialsWarn" + "FoundBuildCredentials": "ms-resource:loc.messages.FoundBuildCredentials", + "NoBuildCredentials": "ms-resource:loc.messages.NoBuildCredentials", + "UnknownCommand": "ms-resource:loc.messages.UnknownCommand", + "MultipleProjectConfigs": "ms-resource:loc.messages.MultipleProjectConfigs", + "ServiceEndpointNotDefined": "ms-resource:loc.messages.ServiceEndpointNotDefined", + "ServiceEndpointUrlNotDefined": "ms-resource:loc.messages.ServiceEndpointUrlNotDefined", + "SavingFile": "ms-resource:loc.messages.SavingFile", + "RestoringFile": "ms-resource:loc.messages.RestoringFile", + "PublishFeed": "ms-resource:loc.messages.PublishFeed", + "PublishExternal": "ms-resource:loc.messages.PublishExternal", + "UseFeed": "ms-resource:loc.messages.UseFeed", + "UseNpmrc": "ms-resource:loc.messages.UseNpmrc", + "PublishRegistry": "ms-resource:loc.messages.PublishRegistry", + "UsingRegistry": "ms-resource:loc.messages.UsingRegistry", + "AddingAuthRegistry": "ms-resource:loc.messages.AddingAuthRegistry", + "FoundLocalRegistries": "ms-resource:loc.messages.FoundLocalRegistries", + "ForcePackagingUrl": "ms-resource:loc.messages.ForcePackagingUrl" } -} \ No newline at end of file +} diff --git a/Tasks/Npm/typings.json b/Tasks/Npm/typings.json index 27eccf12953d..06143c844e9c 100644 --- a/Tasks/Npm/typings.json +++ b/Tasks/Npm/typings.json @@ -1,6 +1,8 @@ { "name": "vsts-npm-task", - "dependencies": {}, + "dependencies": { + "ini": "registry:dt/ini#1.3.3+20160505055005" + }, "globalDependencies": { "mocha": "registry:dt/mocha#2.2.5+20160720003353", "node": "registry:dt/node#6.0.0+20160928143418", diff --git a/Tasks/Npm/typings/index.d.ts b/Tasks/Npm/typings/index.d.ts index bbb3a42c2b21..293634742d49 100644 --- a/Tasks/Npm/typings/index.d.ts +++ b/Tasks/Npm/typings/index.d.ts @@ -1,3 +1,5 @@ /// /// /// +/// +/// diff --git a/Tasks/Npm/typings/modules/ini/index.d.ts b/Tasks/Npm/typings/modules/ini/index.d.ts new file mode 100644 index 000000000000..9334c9be18dd --- /dev/null +++ b/Tasks/Npm/typings/modules/ini/index.d.ts @@ -0,0 +1,20 @@ +// Generated by typings +// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/81862d240d257e28eda42029c4a1bc8bea984360/ini/index.d.ts +declare module 'ini' { +// Type definitions for ini v1.3.3 +// Project: https://github.com/isaacs/ini +// Definitions by: Marcin Porębski +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +interface EncodeOptions { + section: string + whitespace: boolean +} + +export function decode(inistring: string): any; +export function parse(initstring: string): any; +export function encode(object: any, options?: EncodeOptions): string; +export function stringify(object: any, options?: EncodeOptions): string; +export function safe(val: string): string; +export function unsafe(val: string): string; +} diff --git a/Tasks/Npm/typings/modules/ini/typings.json b/Tasks/Npm/typings/modules/ini/typings.json new file mode 100644 index 000000000000..add58c870d11 --- /dev/null +++ b/Tasks/Npm/typings/modules/ini/typings.json @@ -0,0 +1,8 @@ +{ + "resolution": "main", + "tree": { + "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/81862d240d257e28eda42029c4a1bc8bea984360/ini/index.d.ts", + "raw": "registry:dt/ini#1.3.3+20160505055005", + "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/81862d240d257e28eda42029c4a1bc8bea984360/ini/index.d.ts" + } +} diff --git a/Tasks/Npm/typings/modules/mockery/index.d.ts b/Tasks/Npm/typings/modules/mockery/index.d.ts new file mode 100644 index 000000000000..914ea2e73c78 --- /dev/null +++ b/Tasks/Npm/typings/modules/mockery/index.d.ts @@ -0,0 +1,36 @@ +// Generated by typings +// Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/337587de8c13868283993bfacdcdd1a0f3291e7f/mockery/index.d.ts +declare module 'mockery' { +// Type definitions for mockery 1.4.0 +// Project: https://github.com/mfncooper/mockery +// Definitions by: jt000 +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + + + +interface MockeryEnableArgs { + useCleanCache?: boolean; + warnOnReplace?: boolean; + warnOnUnregistered?: boolean; +} + +export function enable(args?: MockeryEnableArgs): void; +export function disable(): void; + +export function registerMock(name: string, mock: any): void; +export function deregisterMock(name: string): void; + +export function registerSubstitute(name: string, substitute: string): void; +export function deregisterSubstitute(name: string): void; + +export function registerAllowable(name: string, unhook?: boolean): void; +export function deregisterAllowable(name: string): void; + +export function registerAllowables(names: string[]): void; +export function deregisterAllowables(names: string[]): void; + +export function deregisterAll(): void; +export function resetCache(): void; +export function warnOnUnregistered(value: boolean): void; +export function warnOnReplace(value: boolean): void; +} diff --git a/Tasks/Npm/typings/modules/mockery/typings.json b/Tasks/Npm/typings/modules/mockery/typings.json new file mode 100644 index 000000000000..69c1599e4c53 --- /dev/null +++ b/Tasks/Npm/typings/modules/mockery/typings.json @@ -0,0 +1,8 @@ +{ + "resolution": "main", + "tree": { + "src": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/337587de8c13868283993bfacdcdd1a0f3291e7f/mockery/index.d.ts", + "raw": "registry:dt/mockery#1.4.0+20160428043022", + "typings": "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/337587de8c13868283993bfacdcdd1a0f3291e7f/mockery/index.d.ts" + } +} diff --git a/Tasks/Npm/util.ts b/Tasks/Npm/util.ts new file mode 100644 index 000000000000..772d34735d42 --- /dev/null +++ b/Tasks/Npm/util.ts @@ -0,0 +1,166 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as Q from 'q'; +import * as url from 'url'; + +import * as tl from 'vsts-task-lib/task'; +import * as vsts from 'vso-node-api/WebApi'; + +import { INpmRegistry, NpmRegistry } from './npmregistry'; +import * as NpmrcParser from './npmrcparser'; + +export function appendToNpmrc(npmrc: string, data: string): void { + tl.writeFile(npmrc, data, { + flag: 'a' + } as tl.FsOptions); +} + +export async function getLocalRegistries(npmrc: string): Promise { + let localRegistries: string[] = []; + let registries = NpmrcParser.GetRegistries(npmrc); + let collectionUrl = url.parse(await getPackagingCollectionUrl()); + + for (let registry of registries) { + let registryUrl = url.parse(registry); + if (registryUrl.host.toLowerCase() === collectionUrl.host.toLowerCase()) { + localRegistries.push(registry); + } + } + + tl.debug(tl.loc('FoundLocalRegistries', localRegistries.length)); + return localRegistries; +} + +export function getFeedIdFromRegistry(registry: string) { + let registryUrl = url.parse(registry); + let registryPathname = registryUrl.pathname.toLowerCase(); + let startingToken = '/_packaging/'; + let startingIndex = registryPathname.indexOf(startingToken); + let endingIndex = registryPathname.indexOf('/npm/registry'); + + return registryUrl.pathname.substring(startingIndex + startingToken.length, endingIndex); +} + +export async function getLocalNpmRegistries(workingDir: string): Promise { + let localNpmRegistries: INpmRegistry[] = []; + let npmrcPath = path.join(workingDir, '.npmrc'); + + if (tl.exist(npmrcPath)) { + let npmRegistries: INpmRegistry[] = []; + for (let registry of await getLocalRegistries(npmrcPath)) { + npmRegistries.push(await NpmRegistry.FromFeedId(getFeedIdFromRegistry(registry), true)); + } + + localNpmRegistries = localNpmRegistries.concat(npmRegistries); + } + + return localNpmRegistries; +} + +export async function getPackagingCollectionUrl(): Promise { + let forcedUrl = tl.getVariable('Npm.PackagingCollectionUrl'); + if (forcedUrl) { + let testUrl = url.parse(forcedUrl); + tl.debug(tl.loc('ForcePackagingUrl', forcedUrl)); + return Q(url.format(testUrl)); + } + + let collectionUrl = url.parse(tl.getVariable('System.TeamFoundationCollectionUri')); + + if (collectionUrl.hostname.toUpperCase().endsWith('.VISUALSTUDIO.COM')) { + let hostParts = collectionUrl.hostname.split('.'); + let packagingHostName = hostParts[0] + '.pkgs.visualstudio.com'; + collectionUrl.hostname = packagingHostName; + // remove the host property so it doesn't override the hostname property for url.format + delete collectionUrl.host; + } + + return Q(url.format(collectionUrl)); +} + +export function getTempNpmrcPath(): string { + let tempUserNpmrcPath: string = path.join(getTempPath(), `${tl.getVariable('Build.BuildId')}.npmrc`); + + return tempUserNpmrcPath; +} + +export function getTempPath(): string { + let tempNpmrcDir + = tl.getVariable('Agent.BuildDirectory') + || tl.getVariable('Agent.ReleaseDirectory') + || process.cwd(); + let tempPath = path.join(tempNpmrcDir, 'npm'); + if (tl.exist(tempPath) === false) { + tl.mkdirP(tempPath); + } + + return tempPath; +} + +function copyFile(src: string, dst: string): void { + let content = fs.readFileSync(src); + fs.writeFileSync(dst, content); +} + +export function saveFile(file: string): void { + if (file && tl.exist(file)) { + let tempPath = getTempPath(); + let baseName = path.basename(file); + let destination = path.join(tempPath, baseName); + + tl.debug(tl.loc('SavingFile', file)); + copyFile(file, destination); + } +} + +export function restoreFile(file: string): void { + if (file) { + let tempPath = getTempPath(); + let baseName = path.basename(file); + let source = path.join(tempPath, baseName); + + if (tl.exist(source)) { + tl.debug(tl.loc('RestoringFile', file)); + copyFile(source, file); + tl.rmRF(source); + } + } +} + +export function getSystemAccessToken(): string { + let auth = tl.getEndpointAuthorization('SYSTEMVSSCONNECTION', false); + if (auth.scheme === 'OAuth') { + tl.debug(tl.loc('FoundBuildCredentials')); + return auth.parameters['AccessToken']; + } else { + tl.warning(tl.loc('NoBuildCredentials')); + } + + return undefined; +} + +export function toNerfDart(uri: string): string { + var parsed = url.parse(uri); + delete parsed.protocol; + delete parsed.auth; + delete parsed.query; + delete parsed.search; + delete parsed.hash; + + return url.resolve(url.format(parsed), '.'); +} + +export async function getFeedRegistryUrl(feedId: string): Promise { + const apiVersion = '3.0-preview.1'; + const area = 'npm'; + const locationId = 'D9B75B07-F1D9-4A67-AAA6-A4D9E66B3352'; + + let accessToken = getSystemAccessToken(); + let credentialHandler = vsts.getBearerHandler(accessToken); + let collectionUrl = await getPackagingCollectionUrl(); + let vssConnection = new vsts.WebApi(collectionUrl, credentialHandler); + let coreApi = vssConnection.getCoreApi(); + let data = await coreApi.vsoClient.getVersioningData(apiVersion, area, locationId, { feedId: feedId }); + + return data.requestUrl; +}