Skip to content

Commit 719bfd7

Browse files
Merge pull request #4398 from aldoms/users/aldoms/npm
Update npm task with install/publish/custom commands
2 parents 56a2903 + d7e901b commit 719bfd7

39 files changed

+1321
-696
lines changed

Tasks/Common/nuget-task-common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
},
1414
"license": "MIT",
1515
"dependencies": {
16-
"mockery": "^2.0.0",
16+
"mockery": "^1.7.0",
1717
"vso-node-api": "^5.0.5",
1818
"vsts-task-lib": "2.0.2-preview",
1919
"vsts-task-tool-lib": "0.4.0"
Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
11
{
22
"loc.friendlyName": "npm",
33
"loc.helpMarkDown": "[More Information](https://go.microsoft.com/fwlink/?LinkID=613746)",
4-
"loc.description": "Run an npm command",
4+
"loc.description": "Install and publish npm packages, or run an npm command. Supports npmjs.com and authenticated registries like Package Management.",
55
"loc.instanceNameFormat": "npm $(command)",
6-
"loc.input.label.cwd": "working folder",
7-
"loc.input.help.cwd": "Working directory where the npm command is run. Defaults to the root of the repo.",
8-
"loc.input.label.command": "npm command",
9-
"loc.input.label.arguments": "arguments",
10-
"loc.input.help.arguments": "Additional arguments passed to npm.",
11-
"loc.messages.InvalidCommand": "Only one command should be used for npm. Use 'Arguments' input for additional arguments.",
12-
"loc.messages.NpmReturnCode": "npm exited with return code: %d",
13-
"loc.messages.NpmFailed": "npm failed with error: %s",
14-
"loc.messages.NpmConfigFailed": "Failed to log the npm config information. Error : %s",
15-
"loc.messages.NpmAuthFailed": "Failed to get the required authentication tokens for npm. Error: %s",
16-
"loc.messages.BuildCredentialsWarn": "Could not determine credentials to use for npm"
17-
}
6+
"loc.group.displayName.customRegistries": "Custom registries and authentication",
7+
"loc.group.displayName.publishRegistries": "Destination registry and authentication",
8+
"loc.input.label.command": "Command",
9+
"loc.input.help.command": "The command and arguments which will be passed to npm for execution.",
10+
"loc.input.label.workingDir": "Working folder with package.json",
11+
"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\".",
12+
"loc.input.label.customCommand": "Command and arguments",
13+
"loc.input.help.customCommand": "Custom command to run, e.g. \"dist-tag ls mypackage\".",
14+
"loc.input.label.customRegistry": "Registries to use",
15+
"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.",
16+
"loc.input.label.customFeed": "Use packages from this VSTS/TFS registry",
17+
"loc.input.help.customFeed": "Include the selected feed in the generated .npmrc.",
18+
"loc.input.label.customEndpoint": "Credentials for registries outside this account/collection",
19+
"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.",
20+
"loc.input.label.publishRegistry": "Registry location",
21+
"loc.input.help.publishRegistry": "Registry the command will target.",
22+
"loc.input.label.publishFeed": "Target registry",
23+
"loc.input.help.publishFeed": "Select a registry hosted in this account. You must have Package Management installed and licensed to select a registry here.",
24+
"loc.input.label.publishEndpoint": "External Registry",
25+
"loc.input.help.publishEndpoint": "Credentials to use for publishing to an external registry.",
26+
"loc.messages.FoundBuildCredentials": "Found build credentials",
27+
"loc.messages.NoBuildCredentials": "Could not find build credentials",
28+
"loc.messages.UnknownCommand": "Unknown command: %s",
29+
"loc.messages.MultipleProjectConfigs": "More than one project .npmrc found in $s",
30+
"loc.messages.ServiceEndpointNotDefined": "Couldn't find Service Endpoint, make sure the selected endpoint still exists.",
31+
"loc.messages.ServiceEndpointUrlNotDefined": "Couldn't find Url for Service Endpoint, make sure Service Endpoint is correctly configured.",
32+
"loc.messages.SavingFile": "Saving file %s",
33+
"loc.messages.RestoringFile": "Restoring file %s",
34+
"loc.messages.PublishFeed": "Publishing to internal feed",
35+
"loc.messages.PublishExternal": "Publishing to external registry",
36+
"loc.messages.UseFeed": "Using internal feed",
37+
"loc.messages.UseNpmrc": "Using registries in .npmrc",
38+
"loc.messages.PublishRegistry": "Publishing to registry: %s",
39+
"loc.messages.UsingRegistry": "Using registry: %s",
40+
"loc.messages.AddingAuthRegistry": "Adding auth for registry: %s",
41+
"loc.messages.FoundLocalRegistries": "Found %d registries in this account/collection",
42+
"loc.messages.ForcePackagingUrl": "Packaging collection url forced to: %s"
43+
}

Tasks/Npm/Tests/L0.ts

Lines changed: 191 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,256 @@
1-
import * as path from 'path';
21
import * as assert from 'assert';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
import * as Q from 'q';
5+
6+
import * as mockery from 'mockery';
37
import * as ttm from 'vsts-task-lib/mock-test';
4-
import {NpmMockHelper} from './NpmMockHelper';
8+
9+
import { NpmMockHelper } from './NpmMockHelper';
510

611
describe('Npm Task', function () {
712
before(() => {
13+
mockery.enable({
14+
useCleanCache: true,
15+
warnOnUnregistered: false
16+
} as mockery.MockeryEnableArgs);
817
});
918

1019
after(() => {
20+
mockery.disable();
21+
});
22+
23+
beforeEach(() => {
24+
mockery.resetCache();
25+
});
26+
27+
afterEach(() => {
28+
mockery.deregisterAll();
1129
});
1230

13-
/* Current behavior */
14-
it("should execute 'npm config list' successfully", (done: MochaDone) => {
31+
// custom
32+
it('custom command should return npm version', (done: MochaDone) => {
1533
this.timeout(1000);
16-
let tp = path.join(__dirname, 'test-configlist.js')
17-
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
34+
let tp = path.join(__dirname, 'custom-version.js');
35+
let tr = new ttm.MockTestRunner(tp);
1836

1937
tr.run();
2038

21-
assert.equal(tr.invokedToolCount, 3, 'should have run vsts-npm-auth, npm config list and npm command');
22-
assert(tr.ran(`${NpmMockHelper.NpmCmdPath} config list`), 'it should have run npm');
23-
assert(tr.stdOutContained('; cli configs'), "should have npm config output");
24-
assert.equal(tr.errorIssues.length, 0, "should have no errors");
25-
// This assert is skipped due to a test mocking issue on non windows platforms.
26-
// assert.equal(tr.warningIssues.length, 0, "should have no warnings: " + tr.warningIssues.join(','));
27-
assert(tr.succeeded, 'should have succeeded');
39+
assert.equal(tr.invokedToolCount, 2, 'task should have run npm');
40+
assert(tr.succeeded, 'task should have succeeded');
41+
assert(tr.stdOutContained('; debug cli configs'), 'should have debug npm config output');
42+
assert(tr.stdOutContained('; cli configs') === false, 'should not have regular npm config output');
2843

2944
done();
3045
});
31-
32-
it('should pass when no arguments are supplied', (done: MochaDone) => {
46+
47+
// show config
48+
it('should execute \'npm config list\' without debug switch', (done: MochaDone) => {
3349
this.timeout(1000);
34-
let tp = path.join(__dirname, 'test-commandWithoutArguments.js')
35-
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
50+
let tp = path.join(__dirname, 'config-noDebug.js');
51+
let tr = new ttm.MockTestRunner(tp);
3652

3753
tr.run();
3854

39-
assert.equal(tr.invokedToolCount, 3, 'should have run npm');
40-
assert(tr.ran(`${NpmMockHelper.NpmCmdPath} root`), 'it should have run npm');
41-
assert(tr.stdOutContained(`${NpmMockHelper.FakeWorkingDirectory}`), "should have npm root output - working directory");
42-
assert(tr.stdOutContained("node_modules"), "should have npm root output - 'node_modules' directory");
43-
assert.equal(tr.errorIssues.length, 0, "should have no errors");
44-
// This assert is skipped due to a test mocking issue on non windows platforms.
45-
// assert.equal(tr.warningIssues.length, 0, "should have no warnings: " + tr.warningIssues.join(','));
46-
assert(tr.succeeded, 'should have succeeded');
55+
assert.equal(tr.invokedToolCount, 2, 'task should have run npm');
56+
assert(tr.succeeded, 'task should have succeeded');
57+
assert(tr.stdOutContained('; cli configs'), 'should have regular npm config output');
58+
assert(tr.stdOutContained('; debug cli configs') === false, 'should not have debug npm config output');
4759

4860
done();
4961
});
50-
51-
it('should fail when command contains spaces', (done: MochaDone) => {
62+
63+
// install command
64+
it('should fail when npm fails', (done: MochaDone) => {
5265
this.timeout(1000);
53-
let tp = path.join(__dirname, 'test-commandContainsSpaces.js')
54-
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
66+
let tp = path.join(__dirname, 'install-npmFailure.js');
67+
let tr = new ttm.MockTestRunner(tp);
5568

5669
tr.run();
5770

58-
assert.equal(tr.invokedToolCount, 0, 'should not have run npm');
59-
assert(tr.failed, 'should have failed');
71+
assert(tr.failed, 'task should have failed');
6072

6173
done();
6274
});
63-
64-
it('should fail when task fails', (done: MochaDone) => {
75+
76+
it ('install using local feed', (done: MochaDone) => {
6577
this.timeout(1000);
66-
let tp = path.join(__dirname, 'test-npmFailure.js')
67-
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
78+
let tp = path.join(__dirname, 'install-feed.js');
79+
let tr = new ttm.MockTestRunner(tp);
6880

6981
tr.run();
7082

71-
assert.equal(tr.invokedToolCount, 3, 'should have run npm');
72-
assert(tr.failed, 'should have failed');
83+
assert.equal(tr.invokedToolCount, 2, 'task should have run npm');
84+
assert(tr.stdOutContained('npm install successful'), 'npm should have installed the package');
85+
assert(tr.succeeded, 'task should have succeeded');
7386

7487
done();
7588
});
7689

77-
/* Deprecated behavior */
78-
it("should execute 'npm config list' successfully (deprecated task)", (done: MochaDone) => {
90+
it ('install using npmrc', (done: MochaDone) => {
7991
this.timeout(1000);
80-
let tp = path.join(__dirname, 'test-configlist-deprecated.js')
81-
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
92+
let tp = path.join(__dirname, 'install-npmrc.js');
93+
let tr = new ttm.MockTestRunner(tp);
8294

8395
tr.run();
8496

85-
assert.equal(tr.invokedToolCount, 1, 'should have run npm');
86-
assert(tr.ran(`${NpmMockHelper.NpmCmdPath} config list`), 'it should have run npm');
87-
assert(tr.stdOutContained('; cli configs'), "should have npm config output");
88-
assert.equal(tr.errorIssues.length, 0, "should have no errors");
89-
assert.equal(tr.warningIssues.length, 0, "should have no warnings: " + tr.warningIssues.join(','));
90-
assert(tr.succeeded, 'should have succeeded');
97+
assert.equal(tr.invokedToolCount, 2, 'task should have run npm');
98+
assert(tr.stdOutContained('npm install successful'), 'npm should have installed the package');
99+
assert(tr.succeeded, 'task should have succeeded');
91100

92101
done();
93102
});
94-
95-
it('should pass when no arguments are supplied (deprecated task)', (done: MochaDone) => {
103+
104+
// publish
105+
it ('publish using feed', (done: MochaDone) => {
96106
this.timeout(1000);
97-
let tp = path.join(__dirname, 'test-commandWithoutArguments-deprecated.js')
98-
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
107+
let tp = path.join(__dirname, 'publish-feed.js');
108+
let tr = new ttm.MockTestRunner(tp);
99109

100110
tr.run();
101111

102-
assert.equal(tr.invokedToolCount, 1, 'should have run npm');
103-
assert(tr.ran(`${NpmMockHelper.NpmCmdPath} root`), 'it should have run npm');
104-
assert(tr.stdOutContained(`${NpmMockHelper.FakeWorkingDirectory}`), "should have npm root output - working directory");
105-
assert(tr.stdOutContained("node_modules"), "should have npm root output - 'node_modules' directory");
106-
assert.equal(tr.errorIssues.length, 0, "should have no errors");
107-
assert.equal(tr.warningIssues.length, 0, "should have no warnings: " + tr.warningIssues.join(','));
108-
assert(tr.succeeded, 'should have succeeded');
112+
assert.equal(tr.invokedToolCount, 2, 'task should have run npm');
113+
assert(tr.stdOutContained('npm publish successful'), 'npm should have installed the package');
114+
assert(tr.succeeded, 'task should have succeeded');
109115

110116
done();
111117
});
112-
113-
it('should fail when command contains spaces (deprecated task)', (done: MochaDone) => {
118+
119+
it ('publish using external registry', (done: MochaDone) => {
114120
this.timeout(1000);
115-
let tp = path.join(__dirname, 'test-commandContainsSpaces-deprecated.js')
116-
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
121+
let tp = path.join(__dirname, 'publish-external.js');
122+
let tr = new ttm.MockTestRunner(tp);
117123

118124
tr.run();
119125

120-
assert.equal(tr.invokedToolCount, 0, 'should not have run npm');
121-
assert(tr.failed, 'should have failed');
126+
assert.equal(tr.invokedToolCount, 2, 'task should have run npm');
127+
assert(tr.stdOutContained('npm publish successful'), 'npm should have installed the package');
128+
assert(tr.succeeded, 'task should have succeeded');
122129

123130
done();
124131
});
125-
126-
it('should fail when task fails (deprecated task)', (done: MochaDone) => {
127-
this.timeout(1000);
128-
let tp = path.join(__dirname, 'test-npmFailure-deprecated.js')
129-
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
130132

131-
tr.run();
133+
// util
134+
it('gets npm registries', (done: MochaDone) => {
135+
let mockTask = {
136+
writeFile: (file: string, data: string | Buffer) => {
137+
// no-op
138+
}
139+
};
140+
mockery.registerMock('vsts-task-lib/task', mockTask);
141+
let npmrc = `registry=http://example.com
142+
always-auth=true
143+
@scoped:registry=http://scoped.com
144+
//scoped.com/:_authToken=thisIsASecretToken
145+
@scopedTwo:registry=http://scopedTwo.com
146+
; some comments
147+
@scoped:always-auth=true
148+
# more comments`;
149+
150+
let mockFs = {
151+
readFileSync: (path: string) => npmrc
152+
};
153+
mockery.registerMock('fs', mockFs);
132154

133-
assert.equal(tr.invokedToolCount, 1, 'should have run npm');
134-
assert(tr.failed, 'should have failed');
155+
let npmrcParser = require('../npmrcparser');
156+
let registries = npmrcParser.GetRegistries('');
157+
158+
assert.equal(registries.length, 3);
159+
assert.equal(registries[0], 'http://example.com/');
160+
assert.equal(registries[1], 'http://scoped.com/');
161+
assert.equal(registries[2], 'http://scopedTwo.com/');
162+
163+
done();
164+
});
165+
166+
it('gets feed id from VSTS registry', (done: MochaDone) => {
167+
mockery.registerMock('vsts-task-lib/task', {});
168+
let util = require('../util');
169+
170+
assert.equal(util.getFeedIdFromRegistry(
171+
'http://account.visualstudio.com/_packaging/feedId/npm/registry'),
172+
'feedId');
173+
assert.equal(util.getFeedIdFromRegistry(
174+
'http://account.visualstudio.com/_packaging/feedId/npm/registry/'),
175+
'feedId');
176+
assert.equal(util.getFeedIdFromRegistry(
177+
'http://TFSSERVER/_packaging/feedId/npm/registry'),
178+
'feedId');
179+
assert.equal(util.getFeedIdFromRegistry(
180+
'http://TFSSERVER:1234/_packaging/feedId/npm/registry'),
181+
'feedId');
135182

136183
done();
137184
});
138-
});
185+
186+
it('gets correct packaging Url', () => {
187+
let mockTask = {
188+
getVariable: (v: string) => {
189+
if (v === 'System.TeamFoundationCollectionUri') {
190+
return 'http://example.visualstudio.com';
191+
}
192+
},
193+
debug: (message: string) => {
194+
// no-op
195+
},
196+
loc: (key: string) => {
197+
// no-op
198+
}
199+
};
200+
mockery.registerMock('vsts-task-lib/task', mockTask);
201+
let util = require('../util');
202+
203+
return util.getPackagingCollectionUrl().then(u => {
204+
assert.equal(u, 'http://example.pkgs.visualstudio.com/'.toLowerCase());
205+
206+
mockTask.getVariable = (v: string) => 'http://TFSSERVER.com/';
207+
return util.getPackagingCollectionUrl().then(u => {
208+
assert.equal(u, 'http://TFSSERVER.com/'.toLowerCase());
209+
210+
mockTask.getVariable = (v: string) => 'http://serverWithPort:1234';
211+
return util.getPackagingCollectionUrl().then(u => {
212+
assert.equal(u, 'http://serverWithPort:1234/'.toLowerCase());
213+
214+
return;
215+
});
216+
});
217+
});
218+
});
219+
220+
it('gets correct local registries', () => {
221+
let mockParser = {
222+
GetRegistries: (npmrc: string) => [
223+
'http://registry.com/npmRegistry/',
224+
'http://example.pkgs.visualstudio.com/npmRegistry/',
225+
'http://localTFSServer/npmRegistry/'
226+
]
227+
};
228+
mockery.registerMock('./npmrcparser', mockParser);
229+
let mockTask = {
230+
getVariable: (v: string) => {
231+
if (v === 'System.TeamFoundationCollectionUri') {
232+
return 'http://example.visualstudio.com';
233+
}
234+
},
235+
debug: (message: string) => {
236+
// no-op
237+
},
238+
loc: (key: string) => {
239+
// no-op
240+
}
241+
};
242+
mockery.registerMock('vsts-task-lib/task', mockTask);
243+
let util = require('../util');
244+
245+
return util.getLocalRegistries('').then((registries: string[]) => {
246+
assert.equal(registries.length, 1);
247+
assert.equal(registries[0], 'http://example.pkgs.visualstudio.com/npmRegistry/');
248+
249+
mockTask.getVariable = () => 'http://localTFSServer/';
250+
return util.getLocalRegistries('').then((registries: string[]) => {
251+
assert.equal(registries.length, 1);
252+
assert.equal(registries[0], 'http://localTFSServer/npmRegistry/');
253+
});
254+
});
255+
});
256+
});

0 commit comments

Comments
 (0)