@percy/client 1.0.0-beta.7 → 1.0.0-beta.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -28
- package/dist/client.js +130 -116
- package/dist/index.js +10 -2
- package/dist/request.js +303 -0
- package/dist/utils.js +31 -85
- package/package.json +21 -14
- package/test/helpers.js +81 -0
package/README.md
CHANGED
|
@@ -1,51 +1,82 @@
|
|
|
1
1
|
# @percy/client
|
|
2
2
|
|
|
3
3
|
Communicate with Percy's API to create builds and snapshots, upload resources, and finalize builds
|
|
4
|
-
and snapshots. Uses `@percy/env` to send environment information with new
|
|
5
|
-
to query for a project's builds using a read access token.
|
|
4
|
+
and snapshots. Uses [`@percy/env`](.packages/env) to send environment information with new
|
|
5
|
+
builds. Can also be used to query for a project's builds using a read access token.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- [Usage](#usage)
|
|
8
|
+
- [Create a build](#create-a-build)
|
|
9
|
+
- [Create, upload, and finalize snapshots](#create-upload-and-finalize-snapshots)
|
|
10
|
+
- [Finalize a build](#finalize-a-build)
|
|
11
|
+
- [Query for a build*](#query-for-a-build)
|
|
12
|
+
- [Query for a project's builds*](#query-for-a-projects-builds)
|
|
13
|
+
- [Wait for a build to be finished*](#wait-for-a-build-to-be-finished)
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
## Usage
|
|
10
16
|
|
|
11
17
|
``` js
|
|
12
|
-
import PercyClient from '@percy/client'
|
|
18
|
+
import PercyClient from '@percy/client'
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
const client = new PercyClient({ token: 'abcdef123456' })
|
|
20
|
+
const client = new PercyClient(options)
|
|
16
21
|
```
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
#### Options
|
|
24
|
+
|
|
25
|
+
- `token` — Your project's `PERCY_TOKEN` (**default** `process.env.PERCY_TOKEN`)
|
|
26
|
+
- `clientInfo` — Client info sent to Percy via a user-agent string
|
|
27
|
+
- `environmentInfo` — Environment info also sent with the user-agent string
|
|
28
|
+
|
|
29
|
+
## Create a build
|
|
30
|
+
|
|
31
|
+
Creates a percy build. Only one build can be created at a time per instance. During this step,
|
|
32
|
+
various environment information is collected via [`@percy/env`](./packages/env#readme) and
|
|
33
|
+
associated with the new build. If `PERCY_PARALLEL_TOTAL` and `PERCY_PARALLEL_NONCE` are present, a
|
|
34
|
+
build shard is created as part of a parallelized Percy build.
|
|
19
35
|
|
|
20
36
|
``` js
|
|
21
37
|
await client.createBuild()
|
|
22
38
|
```
|
|
23
39
|
|
|
24
|
-
|
|
40
|
+
## Create, upload, and finalize snapshots
|
|
41
|
+
|
|
42
|
+
This method combines the work of creating a snapshot, uploading any missing resources, and finally
|
|
43
|
+
finalizng the snapshot.
|
|
25
44
|
|
|
26
45
|
``` js
|
|
27
|
-
await client.sendSnapshot(
|
|
28
|
-
name,
|
|
29
|
-
widths,
|
|
30
|
-
minimumHeight,
|
|
31
|
-
enableJavaScript,
|
|
32
|
-
clientInfo,
|
|
33
|
-
environmentInfo,
|
|
34
|
-
// `sha` falls back to `content` sha
|
|
35
|
-
resources: [{ url, sha, content, mimetype, root }]
|
|
36
|
-
})
|
|
46
|
+
await client.sendSnapshot(buildId, snapshotOptions)
|
|
37
47
|
```
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
#### Options
|
|
50
|
+
|
|
51
|
+
- `name` — Snapshot name
|
|
52
|
+
- `widths` — Widths to take screenshots at
|
|
53
|
+
- `minHeight` — Miniumum screenshot height
|
|
54
|
+
- `enableJavaScript` — Enable JavaScript for screenshots
|
|
55
|
+
- `clientInfo` — Additional client info
|
|
56
|
+
- `environmentInfo` — Additional environment info
|
|
57
|
+
- `resources` — Array of snapshot resources
|
|
58
|
+
- `url` — Resource URL (**required**)
|
|
59
|
+
- `mimetype` — Resource mimetype (**required**)
|
|
60
|
+
- `content` — Resource content (**required**)
|
|
61
|
+
- `sha` — Resource content sha
|
|
62
|
+
- `root` — Boolean indicating a root resource
|
|
63
|
+
|
|
64
|
+
## Finalize a build
|
|
65
|
+
|
|
66
|
+
Finalizes a build. When `all` is true, `all-shards=true` is added as a query param so the
|
|
67
|
+
API finalizes all other parallel build shards associated with the build.
|
|
40
68
|
|
|
41
69
|
``` js
|
|
42
|
-
|
|
70
|
+
// finalize a build
|
|
71
|
+
await client.finalizeBuild(buildId)
|
|
43
72
|
|
|
44
73
|
// finalize all parallel build shards
|
|
45
|
-
await client.finalizeBuild({ all: true })
|
|
74
|
+
await client.finalizeBuild(buildId, { all: true })
|
|
46
75
|
```
|
|
47
76
|
|
|
48
|
-
|
|
77
|
+
## Query for a build
|
|
78
|
+
|
|
79
|
+
Retrieves build data by id.
|
|
49
80
|
|
|
50
81
|
**Requires a read access token**
|
|
51
82
|
|
|
@@ -53,19 +84,51 @@ await client.finalizeBuild({ all: true })
|
|
|
53
84
|
await client.getBuild(buildId)
|
|
54
85
|
```
|
|
55
86
|
|
|
56
|
-
|
|
87
|
+
## Query for a project's builds
|
|
88
|
+
|
|
89
|
+
Retrieves project builds, optionally filtered. The project slug can be found as part of the
|
|
90
|
+
project's URL. For example, the project slug for `https://percy.io/percy/example` is
|
|
91
|
+
`"percy/example"`.
|
|
57
92
|
|
|
58
93
|
**Requires a read access token**
|
|
59
94
|
|
|
60
95
|
``` js
|
|
61
|
-
|
|
96
|
+
// get all builds for a project
|
|
97
|
+
await client.getBuilds(projectSlug)
|
|
98
|
+
|
|
99
|
+
// get all builds for a project's "master" branch
|
|
100
|
+
await client.getBuilds(projectSlug, { branch: 'master' })
|
|
62
101
|
```
|
|
63
102
|
|
|
64
|
-
|
|
103
|
+
#### Filters
|
|
104
|
+
|
|
105
|
+
- `sha` — A single commit sha
|
|
106
|
+
- `shas` — An array of commit shas
|
|
107
|
+
- `branch` — The name of a branch
|
|
108
|
+
- `state` — The build state (`"pending"`, `"finished"`, etc.)
|
|
109
|
+
|
|
110
|
+
## Wait for a build to be finished
|
|
111
|
+
|
|
112
|
+
This method resolves when the build has finished and is no longer pending or processing. By default,
|
|
113
|
+
it will time out if there is no update after 10 minutes.
|
|
65
114
|
|
|
66
115
|
**Requires a read access token**
|
|
67
116
|
|
|
68
117
|
``` js
|
|
69
|
-
|
|
70
|
-
await client.waitForBuild({
|
|
118
|
+
// wait for a specific project build by commit sha
|
|
119
|
+
await client.waitForBuild({
|
|
120
|
+
project: 'percy/example',
|
|
121
|
+
commit: '40-char-sha'
|
|
122
|
+
}, data => {
|
|
123
|
+
// called whenever data changes
|
|
124
|
+
console.log(JSON.stringify(data));
|
|
125
|
+
})
|
|
71
126
|
```
|
|
127
|
+
|
|
128
|
+
#### Options
|
|
129
|
+
|
|
130
|
+
- `build` — Build ID (**required** when missing `commit`)
|
|
131
|
+
- `commit` — Commit SHA (**required** when missing `build`)
|
|
132
|
+
- `project` — Project slug (**required** when using `commit`)
|
|
133
|
+
- `timeout` — Timeout in milliseconds to wait with no updates (**default** `10 * 60 * 1000`)
|
|
134
|
+
- `interval` — Interval in miliseconds to check for updates (**default** `1000`)
|
package/dist/client.js
CHANGED
|
@@ -3,66 +3,94 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.default = void 0;
|
|
6
|
+
exports.default = exports.PercyClient = void 0;
|
|
7
7
|
|
|
8
8
|
var _env = _interopRequireDefault(require("@percy/env"));
|
|
9
9
|
|
|
10
|
-
var
|
|
10
|
+
var _utils = require("@percy/env/dist/utils");
|
|
11
|
+
|
|
12
|
+
var _logger = _interopRequireDefault(require("@percy/logger"));
|
|
11
13
|
|
|
12
14
|
var _package = _interopRequireDefault(require("../package.json"));
|
|
13
15
|
|
|
14
|
-
var
|
|
16
|
+
var _utils2 = require("./utils");
|
|
17
|
+
|
|
18
|
+
var _request = _interopRequireDefault(require("./request"));
|
|
15
19
|
|
|
16
20
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
23
|
+
|
|
24
|
+
// Default client API URL can be set with an env var for API development
|
|
25
|
+
const {
|
|
26
|
+
PERCY_CLIENT_API_URL = 'https://percy.io/api/v1'
|
|
27
|
+
} = process.env; // Validate build ID arguments
|
|
28
|
+
|
|
29
|
+
function validateBuildId(id) {
|
|
30
|
+
if (!id) throw new Error('Missing build ID');
|
|
31
|
+
|
|
32
|
+
if (!(typeof id === 'string' || typeof id === 'number')) {
|
|
33
|
+
throw new Error('Invalid build ID');
|
|
34
|
+
}
|
|
35
|
+
} // Validate project path arguments
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
function validateProjectPath(path) {
|
|
39
|
+
if (!path) throw new Error('Missing project path');
|
|
40
|
+
|
|
41
|
+
if (!/^[^/]+?\/.+/.test(path)) {
|
|
42
|
+
throw new Error(`Invalid project path. Expected "org/project" but received "${path}"`);
|
|
43
|
+
}
|
|
44
|
+
} // PercyClient is used to communicate with the Percy API to create and finalize
|
|
19
45
|
// builds and snapshot. Uses @percy/env to collect environment information used
|
|
20
46
|
// during build creation.
|
|
47
|
+
|
|
48
|
+
|
|
21
49
|
class PercyClient {
|
|
22
50
|
constructor({
|
|
23
51
|
// read or write token, defaults to PERCY_TOKEN environment variable
|
|
24
52
|
token,
|
|
25
53
|
// initial user agent info
|
|
26
|
-
clientInfo
|
|
27
|
-
environmentInfo
|
|
28
|
-
// versioned
|
|
29
|
-
apiUrl =
|
|
54
|
+
clientInfo,
|
|
55
|
+
environmentInfo,
|
|
56
|
+
// versioned api url
|
|
57
|
+
apiUrl = PERCY_CLIENT_API_URL
|
|
30
58
|
} = {}) {
|
|
59
|
+
_defineProperty(this, "log", (0, _logger.default)('client'));
|
|
60
|
+
|
|
61
|
+
_defineProperty(this, "env", new _env.default(process.env));
|
|
62
|
+
|
|
63
|
+
_defineProperty(this, "clientInfo", new Set());
|
|
64
|
+
|
|
65
|
+
_defineProperty(this, "environmentInfo", new Set());
|
|
66
|
+
|
|
31
67
|
Object.assign(this, {
|
|
32
68
|
token,
|
|
33
|
-
apiUrl
|
|
34
|
-
httpAgent: (0, _utils.httpAgentFor)(apiUrl),
|
|
35
|
-
clientInfo: [].concat(clientInfo),
|
|
36
|
-
environmentInfo: [].concat(environmentInfo),
|
|
37
|
-
env: new _env.default(process.env),
|
|
38
|
-
// build info is stored for reference
|
|
39
|
-
build: {
|
|
40
|
-
id: null,
|
|
41
|
-
number: null,
|
|
42
|
-
url: null
|
|
43
|
-
}
|
|
69
|
+
apiUrl
|
|
44
70
|
});
|
|
71
|
+
this.addClientInfo(clientInfo);
|
|
72
|
+
this.addEnvironmentInfo(environmentInfo);
|
|
45
73
|
} // Adds additional unique client info.
|
|
46
74
|
|
|
47
75
|
|
|
48
76
|
addClientInfo(info) {
|
|
49
|
-
|
|
50
|
-
this.clientInfo.
|
|
77
|
+
for (let i of [].concat(info)) {
|
|
78
|
+
if (i) this.clientInfo.add(i);
|
|
51
79
|
}
|
|
52
80
|
} // Adds additional unique environment info.
|
|
53
81
|
|
|
54
82
|
|
|
55
83
|
addEnvironmentInfo(info) {
|
|
56
|
-
|
|
57
|
-
this.environmentInfo.
|
|
84
|
+
for (let i of [].concat(info)) {
|
|
85
|
+
if (i) this.environmentInfo.add(i);
|
|
58
86
|
}
|
|
59
87
|
} // Stringifies client and environment info.
|
|
60
88
|
|
|
61
89
|
|
|
62
90
|
userAgent() {
|
|
63
|
-
let client = [`Percy/${/\w+$/.exec(this.apiUrl)}`].concat(`${_package.default.name}/${_package.default.version}`, this.clientInfo).filter(Boolean)
|
|
64
|
-
let environment = this.environmentInfo.concat(
|
|
65
|
-
return `${client} (${environment})`;
|
|
91
|
+
let client = new Set([`Percy/${/\w+$/.exec(this.apiUrl)}`].concat(`${_package.default.name}/${_package.default.version}`, ...this.clientInfo).filter(Boolean));
|
|
92
|
+
let environment = new Set([...this.environmentInfo].concat(`node/${process.version}`, this.env.info).filter(Boolean));
|
|
93
|
+
return `${[...client].join(' ')} (${[...environment].join('; ')})`;
|
|
66
94
|
} // Checks for a Percy token and returns it.
|
|
67
95
|
|
|
68
96
|
|
|
@@ -84,36 +112,21 @@ class PercyClient {
|
|
|
84
112
|
|
|
85
113
|
|
|
86
114
|
get(path) {
|
|
87
|
-
return (0,
|
|
115
|
+
return (0, _request.default)(`${this.apiUrl}/${path}`, {
|
|
88
116
|
method: 'GET',
|
|
89
|
-
agent: this.httpAgent,
|
|
90
117
|
headers: this.headers()
|
|
91
118
|
});
|
|
92
119
|
} // Performs a POST request to a JSON API endpoint with appropriate headers.
|
|
93
120
|
|
|
94
121
|
|
|
95
122
|
post(path, body = {}) {
|
|
96
|
-
return (0,
|
|
123
|
+
return (0, _request.default)(`${this.apiUrl}/${path}`, {
|
|
97
124
|
method: 'POST',
|
|
98
|
-
agent: this.httpAgent,
|
|
99
125
|
body: JSON.stringify(body),
|
|
100
126
|
headers: this.headers({
|
|
101
127
|
'Content-Type': 'application/vnd.api+json'
|
|
102
128
|
})
|
|
103
129
|
});
|
|
104
|
-
} // Sets build reference data or nullifies it when no data is provided.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
setBuildData(data) {
|
|
108
|
-
var _data$attributes, _data$attributes2;
|
|
109
|
-
|
|
110
|
-
return Object.assign(this, {
|
|
111
|
-
build: {
|
|
112
|
-
id: data === null || data === void 0 ? void 0 : data.id,
|
|
113
|
-
number: data === null || data === void 0 ? void 0 : (_data$attributes = data.attributes) === null || _data$attributes === void 0 ? void 0 : _data$attributes['build-number'],
|
|
114
|
-
url: data === null || data === void 0 ? void 0 : (_data$attributes2 = data.attributes) === null || _data$attributes2 === void 0 ? void 0 : _data$attributes2['web-url']
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
130
|
} // Creates a build with optional build resources. Only one build can be
|
|
118
131
|
// created at a time per instance so snapshots and build finalization can be
|
|
119
132
|
// done more seemlessly without manually tracking build ids
|
|
@@ -122,11 +135,8 @@ class PercyClient {
|
|
|
122
135
|
async createBuild({
|
|
123
136
|
resources = []
|
|
124
137
|
} = {}) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let body = await this.post('builds', {
|
|
138
|
+
this.log.debug('Creating a new build...');
|
|
139
|
+
return this.post('builds', {
|
|
130
140
|
data: {
|
|
131
141
|
type: 'builds',
|
|
132
142
|
attributes: {
|
|
@@ -149,7 +159,7 @@ class PercyClient {
|
|
|
149
159
|
resources: {
|
|
150
160
|
data: resources.map(r => ({
|
|
151
161
|
type: 'resources',
|
|
152
|
-
id: r.sha || (0,
|
|
162
|
+
id: r.sha || (0, _utils2.sha256hash)(r.content),
|
|
153
163
|
attributes: {
|
|
154
164
|
'resource-url': r.url,
|
|
155
165
|
'is-root': r.root || null,
|
|
@@ -160,34 +170,32 @@ class PercyClient {
|
|
|
160
170
|
}
|
|
161
171
|
}
|
|
162
172
|
});
|
|
163
|
-
this.setBuildData(body === null || body === void 0 ? void 0 : body.data);
|
|
164
|
-
return body;
|
|
165
173
|
} // Finalizes the active build. When `all` is true, `all-shards=true` is
|
|
166
174
|
// added as a query param so the API finalizes all other build shards.
|
|
167
175
|
|
|
168
176
|
|
|
169
|
-
async finalizeBuild({
|
|
177
|
+
async finalizeBuild(buildId, {
|
|
170
178
|
all = false
|
|
171
179
|
} = {}) {
|
|
172
|
-
|
|
173
|
-
throw new Error('This client instance has no active build');
|
|
174
|
-
}
|
|
175
|
-
|
|
180
|
+
validateBuildId(buildId);
|
|
176
181
|
let qs = all ? 'all-shards=true' : '';
|
|
177
|
-
|
|
178
|
-
this.
|
|
179
|
-
return body;
|
|
182
|
+
this.log.debug(`Finalizing build ${buildId}...`);
|
|
183
|
+
return this.post(`builds/${buildId}/finalize?${qs}`);
|
|
180
184
|
} // Retrieves build data by id. Requires a read access token.
|
|
181
185
|
|
|
182
186
|
|
|
183
187
|
async getBuild(buildId) {
|
|
188
|
+
validateBuildId(buildId);
|
|
189
|
+
this.log.debug(`Get build ${buildId}`);
|
|
184
190
|
return this.get(`builds/${buildId}`);
|
|
185
191
|
} // Retrieves project builds optionally filtered. Requires a read access token.
|
|
186
192
|
|
|
187
193
|
|
|
188
|
-
async getBuilds(
|
|
194
|
+
async getBuilds(project, filters = {}) {
|
|
195
|
+
validateProjectPath(project);
|
|
189
196
|
let qs = Object.keys(filters).map(k => Array.isArray(filters[k]) ? filters[k].map(v => `filter[${k}][]=${v}`).join('&') : `filter[${k}]=${filters[k]}`).join('&');
|
|
190
|
-
|
|
197
|
+
this.log.debug(`Fetching builds for ${project}`);
|
|
198
|
+
return this.get(`projects/${project}/builds?${qs}`);
|
|
191
199
|
} // Resolves when the build has finished and is no longer pending or
|
|
192
200
|
// processing. By default, will time out if no update after 10 minutes.
|
|
193
201
|
|
|
@@ -196,49 +204,52 @@ class PercyClient {
|
|
|
196
204
|
build,
|
|
197
205
|
project,
|
|
198
206
|
commit,
|
|
199
|
-
|
|
200
|
-
timeout = 600000,
|
|
207
|
+
timeout = 10 * 60 * 1000,
|
|
201
208
|
interval = 1000
|
|
202
|
-
}) {
|
|
209
|
+
}, onProgress) {
|
|
203
210
|
if (commit && !project) {
|
|
204
|
-
throw new Error('Missing project for commit');
|
|
211
|
+
throw new Error('Missing project path for commit');
|
|
205
212
|
} else if (!commit && !build) {
|
|
206
213
|
throw new Error('Missing build ID or commit SHA');
|
|
207
|
-
}
|
|
214
|
+
} else if (project) {
|
|
215
|
+
validateProjectPath(project);
|
|
216
|
+
}
|
|
208
217
|
|
|
218
|
+
let sha = commit && ((0, _utils.git)(`rev-parse ${commit}`) || commit);
|
|
209
219
|
|
|
210
|
-
let
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
sha
|
|
214
|
-
});
|
|
215
|
-
let data = build ? body === null || body === void 0 ? void 0 : body.data : body === null || body === void 0 ? void 0 : body.data[0];
|
|
216
|
-
return [data, data === null || data === void 0 ? void 0 : data.attributes.state];
|
|
217
|
-
}; // recursively poll every second until the build finishes
|
|
220
|
+
let fetchData = async () => build ? (await this.getBuild(build)).data : (await this.getBuilds(project, {
|
|
221
|
+
sha
|
|
222
|
+
})).data[0];
|
|
218
223
|
|
|
224
|
+
this.log.debug(`Waiting for build ${build || `${project} (${commit})`}...`); // recursively poll every second until the build finishes
|
|
219
225
|
|
|
220
226
|
return new Promise((resolve, reject) => async function poll(last, t) {
|
|
221
227
|
try {
|
|
222
|
-
let
|
|
223
|
-
let
|
|
224
|
-
let pending = !state || state === 'pending' || state === 'processing';
|
|
228
|
+
let data = await fetchData();
|
|
229
|
+
let state = data === null || data === void 0 ? void 0 : data.attributes.state;
|
|
230
|
+
let pending = !state || state === 'pending' || state === 'processing';
|
|
231
|
+
let updated = JSON.stringify(data) !== JSON.stringify(last); // new data received
|
|
225
232
|
|
|
226
233
|
if (updated) {
|
|
227
234
|
t = Date.now(); // no new data within the timeout
|
|
228
235
|
} else if (Date.now() - t >= timeout) {
|
|
229
236
|
throw new Error('Timeout exceeded without an update');
|
|
230
|
-
} // call progress after the first update
|
|
237
|
+
} // call progress every update after the first update
|
|
231
238
|
|
|
232
239
|
|
|
233
|
-
if ((last || pending) && updated
|
|
234
|
-
|
|
240
|
+
if ((last || pending) && updated) {
|
|
241
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress(data);
|
|
235
242
|
} // not finished, poll again
|
|
236
243
|
|
|
237
244
|
|
|
238
245
|
if (pending) {
|
|
239
246
|
return setTimeout(poll, interval, data, t); // build finished
|
|
240
247
|
} else {
|
|
241
|
-
|
|
248
|
+
// ensure progress is called at least once
|
|
249
|
+
if (!last) onProgress === null || onProgress === void 0 ? void 0 : onProgress(data);
|
|
250
|
+
resolve({
|
|
251
|
+
data
|
|
252
|
+
});
|
|
242
253
|
}
|
|
243
254
|
} catch (err) {
|
|
244
255
|
reject(err);
|
|
@@ -249,70 +260,70 @@ class PercyClient {
|
|
|
249
260
|
// created from `content` if one is not provided.
|
|
250
261
|
|
|
251
262
|
|
|
252
|
-
async uploadResource({
|
|
263
|
+
async uploadResource(buildId, {
|
|
264
|
+
url,
|
|
253
265
|
sha,
|
|
254
266
|
filepath,
|
|
255
267
|
content
|
|
256
|
-
}) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
268
|
+
} = {}) {
|
|
269
|
+
validateBuildId(buildId);
|
|
270
|
+
this.log.debug(`Uploading resource: ${url}...`);
|
|
261
271
|
content = filepath ? require('fs').readFileSync(filepath) : content;
|
|
262
|
-
return this.post(`builds/${
|
|
272
|
+
return this.post(`builds/${buildId}/resources`, {
|
|
263
273
|
data: {
|
|
264
274
|
type: 'resources',
|
|
265
|
-
id: sha || (0,
|
|
275
|
+
id: sha || (0, _utils2.sha256hash)(content),
|
|
266
276
|
attributes: {
|
|
267
|
-
'base64-content': (0,
|
|
277
|
+
'base64-content': (0, _utils2.base64encode)(content)
|
|
268
278
|
}
|
|
269
279
|
}
|
|
270
280
|
});
|
|
271
281
|
} // Uploads resources to the active build concurrently, two at a time.
|
|
272
282
|
|
|
273
283
|
|
|
274
|
-
async uploadResources(resources) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return (0, _utils.pool)(function* () {
|
|
284
|
+
async uploadResources(buildId, resources) {
|
|
285
|
+
validateBuildId(buildId);
|
|
286
|
+
this.log.debug(`Uploading resources for ${buildId}...`);
|
|
287
|
+
return (0, _utils2.pool)(function* () {
|
|
280
288
|
for (let resource of resources) {
|
|
281
|
-
yield this.uploadResource(resource);
|
|
289
|
+
yield this.uploadResource(buildId, resource);
|
|
282
290
|
}
|
|
283
291
|
}, this, 2);
|
|
284
292
|
} // Creates a snapshot for the active build using the provided attributes.
|
|
285
293
|
|
|
286
294
|
|
|
287
|
-
async createSnapshot({
|
|
295
|
+
async createSnapshot(buildId, {
|
|
288
296
|
name,
|
|
289
297
|
widths,
|
|
290
|
-
|
|
298
|
+
minHeight,
|
|
291
299
|
enableJavaScript,
|
|
292
300
|
clientInfo,
|
|
293
301
|
environmentInfo,
|
|
294
302
|
resources = []
|
|
295
303
|
} = {}) {
|
|
296
|
-
|
|
297
|
-
throw new Error('This client instance has no active build');
|
|
298
|
-
}
|
|
299
|
-
|
|
304
|
+
validateBuildId(buildId);
|
|
300
305
|
this.addClientInfo(clientInfo);
|
|
301
306
|
this.addEnvironmentInfo(environmentInfo);
|
|
302
|
-
|
|
307
|
+
|
|
308
|
+
if (!this.clientInfo.size || !this.environmentInfo.size) {
|
|
309
|
+
this.log.warn('Warning: Missing `clientInfo` and/or `environmentInfo` properties');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
this.log.debug(`Creating snapshot: ${name}...`);
|
|
313
|
+
return this.post(`builds/${buildId}/snapshots`, {
|
|
303
314
|
data: {
|
|
304
315
|
type: 'snapshots',
|
|
305
316
|
attributes: {
|
|
306
317
|
name: name || null,
|
|
307
318
|
widths: widths || null,
|
|
308
|
-
'minimum-height':
|
|
319
|
+
'minimum-height': minHeight || null,
|
|
309
320
|
'enable-javascript': enableJavaScript || null
|
|
310
321
|
},
|
|
311
322
|
relationships: {
|
|
312
323
|
resources: {
|
|
313
324
|
data: resources.map(r => ({
|
|
314
325
|
type: 'resources',
|
|
315
|
-
id: r.sha || (0,
|
|
326
|
+
id: r.sha || (0, _utils2.sha256hash)(r.content),
|
|
316
327
|
attributes: {
|
|
317
328
|
'resource-url': r.url || null,
|
|
318
329
|
'is-root': r.root || null,
|
|
@@ -327,31 +338,34 @@ class PercyClient {
|
|
|
327
338
|
|
|
328
339
|
|
|
329
340
|
async finalizeSnapshot(snapshotId) {
|
|
341
|
+
if (!snapshotId) throw new Error('Missing snapshot ID');
|
|
342
|
+
this.log.debug(`Finalizing snapshot ${snapshotId}...`);
|
|
330
343
|
return this.post(`snapshots/${snapshotId}/finalize`);
|
|
331
344
|
} // Convenience method for creating a snapshot for the active build, uploading
|
|
332
345
|
// missing resources for the snapshot, and finalizing the snapshot.
|
|
333
346
|
|
|
334
347
|
|
|
335
|
-
async sendSnapshot(options) {
|
|
336
|
-
var
|
|
348
|
+
async sendSnapshot(buildId, options) {
|
|
349
|
+
var _snapshot$data$relati, _snapshot$data$relati2;
|
|
337
350
|
|
|
338
|
-
let
|
|
339
|
-
|
|
340
|
-
} = await this.createSnapshot(options);
|
|
341
|
-
let missing = (_data$relationships = data.relationships) === null || _data$relationships === void 0 ? void 0 : (_data$relationships$m = _data$relationships['missing-resources']) === null || _data$relationships$m === void 0 ? void 0 : _data$relationships$m.data;
|
|
351
|
+
let snapshot = await this.createSnapshot(buildId, options);
|
|
352
|
+
let missing = (_snapshot$data$relati = snapshot.data.relationships) === null || _snapshot$data$relati === void 0 ? void 0 : (_snapshot$data$relati2 = _snapshot$data$relati['missing-resources']) === null || _snapshot$data$relati2 === void 0 ? void 0 : _snapshot$data$relati2.data;
|
|
342
353
|
|
|
343
|
-
if (missing
|
|
354
|
+
if (missing !== null && missing !== void 0 && missing.length) {
|
|
344
355
|
let resources = options.resources.reduce((acc, r) => Object.assign(acc, {
|
|
345
356
|
[r.sha]: r
|
|
346
357
|
}), {});
|
|
347
|
-
await this.uploadResources(missing.map(({
|
|
358
|
+
await this.uploadResources(buildId, missing.map(({
|
|
348
359
|
id
|
|
349
360
|
}) => resources[id]));
|
|
350
361
|
}
|
|
351
362
|
|
|
352
|
-
await this.finalizeSnapshot(data.id);
|
|
363
|
+
await this.finalizeSnapshot(snapshot.data.id);
|
|
364
|
+
return snapshot;
|
|
353
365
|
}
|
|
354
366
|
|
|
355
367
|
}
|
|
356
368
|
|
|
357
|
-
exports.
|
|
369
|
+
exports.PercyClient = PercyClient;
|
|
370
|
+
var _default = PercyClient;
|
|
371
|
+
exports.default = _default;
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
Object.defineProperty(exports, "PercyClient", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _client.PercyClient;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
6
12
|
Object.defineProperty(exports, "default", {
|
|
7
13
|
enumerable: true,
|
|
8
14
|
get: function () {
|
|
@@ -10,6 +16,8 @@ Object.defineProperty(exports, "default", {
|
|
|
10
16
|
}
|
|
11
17
|
});
|
|
12
18
|
|
|
13
|
-
var _client =
|
|
19
|
+
var _client = _interopRequireWildcard(require("./client"));
|
|
20
|
+
|
|
21
|
+
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
|
14
22
|
|
|
15
|
-
function
|
|
23
|
+
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
package/dist/request.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = exports.ProxyHttpsAgent = exports.ProxyHttpAgent = void 0;
|
|
7
|
+
exports.getProxy = getProxy;
|
|
8
|
+
exports.href = href;
|
|
9
|
+
exports.port = port;
|
|
10
|
+
exports.proxyAgentFor = proxyAgentFor;
|
|
11
|
+
exports.request = request;
|
|
12
|
+
|
|
13
|
+
var _net = _interopRequireDefault(require("net"));
|
|
14
|
+
|
|
15
|
+
var _tls = _interopRequireDefault(require("tls"));
|
|
16
|
+
|
|
17
|
+
var _http = _interopRequireDefault(require("http"));
|
|
18
|
+
|
|
19
|
+
var _https = _interopRequireDefault(require("https"));
|
|
20
|
+
|
|
21
|
+
var _logger = _interopRequireDefault(require("@percy/logger"));
|
|
22
|
+
|
|
23
|
+
var _utils = require("./utils");
|
|
24
|
+
|
|
25
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
26
|
+
|
|
27
|
+
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
28
|
+
|
|
29
|
+
const CRLF = '\r\n';
|
|
30
|
+
const STATUS_REG = /^HTTP\/1.[01] (\d*)/;
|
|
31
|
+
const RETRY_ERROR_CODES = ['ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'EHOSTUNREACH', 'EAI_AGAIN']; // Returns the port number of a URL object. Defaults to port 443 for https
|
|
32
|
+
// protocols or port 80 otherwise.
|
|
33
|
+
|
|
34
|
+
function port(options) {
|
|
35
|
+
if (options.port) return options.port;
|
|
36
|
+
return options.protocol === 'https:' ? 443 : 80;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function href(options) {
|
|
40
|
+
let {
|
|
41
|
+
protocol,
|
|
42
|
+
hostname,
|
|
43
|
+
path,
|
|
44
|
+
pathname,
|
|
45
|
+
search,
|
|
46
|
+
hash
|
|
47
|
+
} = options;
|
|
48
|
+
return `${protocol}//${hostname}:${port(options)}` + (path || `${pathname || ''}${search || ''}${hash || ''}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
;
|
|
52
|
+
|
|
53
|
+
function getProxy(options) {
|
|
54
|
+
let proxyUrl = options.protocol === 'https:' && (process.env.https_proxy || process.env.HTTPS_PROXY) || process.env.http_proxy || process.env.HTTP_PROXY;
|
|
55
|
+
let shouldProxy = !!proxyUrl && !(0, _utils.hostnameMatches)(process.env.no_proxy || process.env.NO_PROXY, href(options));
|
|
56
|
+
|
|
57
|
+
if (shouldProxy) {
|
|
58
|
+
proxyUrl = new URL(proxyUrl);
|
|
59
|
+
let isHttps = proxyUrl.protocol === 'https:';
|
|
60
|
+
|
|
61
|
+
if (!isHttps && proxyUrl.protocol !== 'http:') {
|
|
62
|
+
throw new Error(`Unsupported proxy protocol: ${proxyUrl.protocol}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let proxy = {
|
|
66
|
+
isHttps
|
|
67
|
+
};
|
|
68
|
+
proxy.auth = !!proxyUrl.username && 'Basic ' + (proxyUrl.password ? Buffer.from(`${proxyUrl.username}:${proxyUrl.password}`) : Buffer.from(proxyUrl.username)).toString('base64');
|
|
69
|
+
proxy.host = proxyUrl.hostname;
|
|
70
|
+
proxy.port = port(proxyUrl);
|
|
71
|
+
|
|
72
|
+
proxy.connect = () => (isHttps ? _tls.default : _net.default).connect({
|
|
73
|
+
rejectUnauthorized: options.rejectUnauthorized,
|
|
74
|
+
host: proxy.host,
|
|
75
|
+
port: proxy.port
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return proxy;
|
|
79
|
+
}
|
|
80
|
+
} // Proxified http agent
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ProxyHttpAgent extends _http.default.Agent {
|
|
84
|
+
constructor(...args) {
|
|
85
|
+
super(...args);
|
|
86
|
+
|
|
87
|
+
_defineProperty(this, "httpsAgent", new _https.default.Agent({
|
|
88
|
+
keepAlive: true
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
addRequest(request, options) {
|
|
93
|
+
var _request$outputData;
|
|
94
|
+
|
|
95
|
+
let proxy = getProxy(options);
|
|
96
|
+
if (!proxy) return super.addRequest(request, options);
|
|
97
|
+
(0, _logger.default)('client:proxy').debug(`Proxying request: ${options.href}`); // modify the request for proxying
|
|
98
|
+
|
|
99
|
+
request.path = href(options);
|
|
100
|
+
|
|
101
|
+
if (proxy.auth) {
|
|
102
|
+
request.setHeader('Proxy-Authorization', proxy.auth);
|
|
103
|
+
} // regenerate headers since we just changed things
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
delete request._header;
|
|
107
|
+
|
|
108
|
+
request._implicitHeader();
|
|
109
|
+
|
|
110
|
+
if (((_request$outputData = request.outputData) === null || _request$outputData === void 0 ? void 0 : _request$outputData.length) > 0) {
|
|
111
|
+
let first = request.outputData[0].data;
|
|
112
|
+
let endOfHeaders = first.indexOf(CRLF.repeat(2)) + 4;
|
|
113
|
+
request.outputData[0].data = request._header + first.substring(endOfHeaders);
|
|
114
|
+
} // coerce the connection to the proxy
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
options.port = proxy.port;
|
|
118
|
+
options.host = proxy.host;
|
|
119
|
+
delete options.path;
|
|
120
|
+
|
|
121
|
+
if (proxy.isHttps) {
|
|
122
|
+
// use the underlying https agent to complete the connection
|
|
123
|
+
request.agent = this.httpsAgent;
|
|
124
|
+
return this.httpsAgent.addRequest(request, options);
|
|
125
|
+
} else {
|
|
126
|
+
return super.addRequest(request, options);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
} // Proxified https agent
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
exports.ProxyHttpAgent = ProxyHttpAgent;
|
|
134
|
+
|
|
135
|
+
class ProxyHttpsAgent extends _https.default.Agent {
|
|
136
|
+
constructor(options) {
|
|
137
|
+
// default keep-alive
|
|
138
|
+
super({
|
|
139
|
+
keepAlive: true,
|
|
140
|
+
...options
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
createConnection(options, callback) {
|
|
145
|
+
let proxy = getProxy(options);
|
|
146
|
+
if (!proxy) return super.createConnection(options, callback);
|
|
147
|
+
(0, _logger.default)('client:proxy').debug(`Proxying request: ${href(options)}`); // generate proxy connect message
|
|
148
|
+
|
|
149
|
+
let host = `${options.hostname}:${port(options)}`;
|
|
150
|
+
let connectMessage = [`CONNECT ${host} HTTP/1.1`, `Host: ${host}`];
|
|
151
|
+
|
|
152
|
+
if (proxy.auth) {
|
|
153
|
+
connectMessage.push(`Proxy-Authorization: ${proxy.auth}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
connectMessage = connectMessage.join(CRLF);
|
|
157
|
+
connectMessage += CRLF.repeat(2); // start the proxy connection and setup listeners
|
|
158
|
+
|
|
159
|
+
let socket = proxy.connect();
|
|
160
|
+
|
|
161
|
+
let handleError = err => {
|
|
162
|
+
socket.destroy(err);
|
|
163
|
+
callback(err);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
let handleClose = () => handleError(new Error('Connection closed while sending request to upstream proxy'));
|
|
167
|
+
|
|
168
|
+
let buffer = '';
|
|
169
|
+
|
|
170
|
+
let handleData = data => {
|
|
171
|
+
var _buffer$match;
|
|
172
|
+
|
|
173
|
+
buffer += data.toString(); // haven't received end of headers yet, keep buffering
|
|
174
|
+
|
|
175
|
+
if (!buffer.includes(CRLF.repeat(2))) return; // stop listening after end of headers
|
|
176
|
+
|
|
177
|
+
socket.off('data', handleData);
|
|
178
|
+
|
|
179
|
+
if (((_buffer$match = buffer.match(STATUS_REG)) === null || _buffer$match === void 0 ? void 0 : _buffer$match[1]) !== '200') {
|
|
180
|
+
return handleError(new Error('Error establishing proxy connection. ' + `Response from server was: ${buffer}`));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
options.socket = socket;
|
|
184
|
+
options.servername = options.hostname; // callback not passed in so not to be added as a listener
|
|
185
|
+
|
|
186
|
+
callback(null, super.createConnection(options));
|
|
187
|
+
}; // send and handle the connect message
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
socket.on('error', handleError).on('close', handleClose).on('data', handleData).write(connectMessage);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
exports.ProxyHttpsAgent = ProxyHttpsAgent;
|
|
196
|
+
|
|
197
|
+
function proxyAgentFor(url, options) {
|
|
198
|
+
let cache = proxyAgentFor.cache || (proxyAgentFor.cache = new Map());
|
|
199
|
+
let {
|
|
200
|
+
protocol,
|
|
201
|
+
hostname
|
|
202
|
+
} = new URL(url);
|
|
203
|
+
let cachekey = `${protocol}//${hostname}`;
|
|
204
|
+
|
|
205
|
+
if (!cache.has(cachekey)) {
|
|
206
|
+
cache.set(cachekey, protocol === 'https:' ? new ProxyHttpsAgent(options) : new ProxyHttpAgent(options));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return cache.get(cachekey);
|
|
210
|
+
} // Proxified request function that resolves with the response body when the request is successful
|
|
211
|
+
// and rejects when a non-successful response is received. The rejected error contains response data
|
|
212
|
+
// and any received error details. Server 500 errors are retried up to 5 times at 50ms intervals by
|
|
213
|
+
// default, and 404 errors may also be optionally retried. If a callback is provided, it is called
|
|
214
|
+
// with the parsed response body and response details. If the callback returns a value, that value
|
|
215
|
+
// will be returned in the final resolved promise instead of the response body.
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
function request(url, options = {}, callback) {
|
|
219
|
+
// accept `request(url, callback)`
|
|
220
|
+
if (typeof options === 'function') [options, callback] = [{}, options];
|
|
221
|
+
let {
|
|
222
|
+
body,
|
|
223
|
+
retries,
|
|
224
|
+
retryNotFound,
|
|
225
|
+
interval,
|
|
226
|
+
noProxy,
|
|
227
|
+
...requestOptions
|
|
228
|
+
} = options; // allow bypassing proxied requests entirely
|
|
229
|
+
|
|
230
|
+
if (!noProxy) requestOptions.agent || (requestOptions.agent = proxyAgentFor(url)); // parse the requested URL into request options
|
|
231
|
+
|
|
232
|
+
let {
|
|
233
|
+
protocol,
|
|
234
|
+
hostname,
|
|
235
|
+
port,
|
|
236
|
+
pathname,
|
|
237
|
+
search,
|
|
238
|
+
hash
|
|
239
|
+
} = new URL(url);
|
|
240
|
+
return (0, _utils.retry)((resolve, reject, retry) => {
|
|
241
|
+
let handleError = error => {
|
|
242
|
+
if (handleError.handled) return;
|
|
243
|
+
handleError.handled = true;
|
|
244
|
+
let shouldRetry = error.response // maybe retry 404s and always retry 500s
|
|
245
|
+
? retryNotFound && error.response.status === 404 || error.response.status >= 500 && error.response.status < 600 // retry specific error codes
|
|
246
|
+
: !!error.code && RETRY_ERROR_CODES.includes(error.code);
|
|
247
|
+
return shouldRetry ? retry(error) : reject(error);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
let handleFinished = async (body, res) => {
|
|
251
|
+
let raw = body; // attempt to parse the body as json
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
body = JSON.parse(body);
|
|
255
|
+
} catch (e) {}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
259
|
+
var _await$callback, _callback;
|
|
260
|
+
|
|
261
|
+
// resolve successful statuses after the callback
|
|
262
|
+
resolve((_await$callback = await ((_callback = callback) === null || _callback === void 0 ? void 0 : _callback(body, res))) !== null && _await$callback !== void 0 ? _await$callback : body);
|
|
263
|
+
} else {
|
|
264
|
+
var _body, _body$errors, _body$errors$find;
|
|
265
|
+
|
|
266
|
+
// use the first error detail or the status message
|
|
267
|
+
throw new Error(((_body = body) === null || _body === void 0 ? void 0 : (_body$errors = _body.errors) === null || _body$errors === void 0 ? void 0 : (_body$errors$find = _body$errors.find(e => e.detail)) === null || _body$errors$find === void 0 ? void 0 : _body$errors$find.detail) || `${res.statusCode} ${res.statusMessage || raw}`);
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
handleError(Object.assign(error, {
|
|
271
|
+
response: {
|
|
272
|
+
status: res.statusCode,
|
|
273
|
+
body
|
|
274
|
+
}
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
let handleResponse = res => {
|
|
280
|
+
let body = '';
|
|
281
|
+
res.setEncoding('utf8');
|
|
282
|
+
res.on('data', chunk => body += chunk);
|
|
283
|
+
res.on('end', () => handleFinished(body, res));
|
|
284
|
+
res.on('error', handleError);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
let req = (protocol === 'https:' ? _https.default : _http.default).request({ ...requestOptions,
|
|
288
|
+
path: pathname + search + hash,
|
|
289
|
+
protocol,
|
|
290
|
+
hostname,
|
|
291
|
+
port
|
|
292
|
+
});
|
|
293
|
+
req.on('response', handleResponse);
|
|
294
|
+
req.on('error', handleError);
|
|
295
|
+
req.end(body);
|
|
296
|
+
}, {
|
|
297
|
+
retries,
|
|
298
|
+
interval
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
var _default = request;
|
|
303
|
+
exports.default = _default;
|
package/dist/utils.js
CHANGED
|
@@ -3,16 +3,14 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.sha256hash = sha256hash;
|
|
7
6
|
exports.base64encode = base64encode;
|
|
7
|
+
exports.hostnameMatches = hostnameMatches;
|
|
8
8
|
exports.pool = pool;
|
|
9
|
-
exports.
|
|
10
|
-
exports.
|
|
9
|
+
exports.retry = retry;
|
|
10
|
+
exports.sha256hash = sha256hash;
|
|
11
11
|
|
|
12
12
|
var _crypto = _interopRequireDefault(require("crypto"));
|
|
13
13
|
|
|
14
|
-
var _url = require("url");
|
|
15
|
-
|
|
16
14
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
15
|
|
|
18
16
|
// Returns a sha256 hash of a string.
|
|
@@ -74,17 +72,13 @@ function pool(generator, context, concurrency) {
|
|
|
74
72
|
function retry(fn, {
|
|
75
73
|
retries = 5,
|
|
76
74
|
interval = 50
|
|
77
|
-
}
|
|
75
|
+
}) {
|
|
78
76
|
return new Promise((resolve, reject) => {
|
|
79
|
-
|
|
80
|
-
let run = () => {
|
|
81
|
-
fn(resolve, reject, retry);
|
|
82
|
-
retries--;
|
|
83
|
-
}; // wait an interval to try again or reject with the error
|
|
77
|
+
let run = () => fn(resolve, reject, retry); // wait an interval to try again or reject with the error
|
|
84
78
|
|
|
85
79
|
|
|
86
80
|
let retry = err => {
|
|
87
|
-
if (retries) {
|
|
81
|
+
if (retries-- > 0) {
|
|
88
82
|
setTimeout(run, interval);
|
|
89
83
|
} else {
|
|
90
84
|
reject(err);
|
|
@@ -94,83 +88,35 @@ function retry(fn, {
|
|
|
94
88
|
|
|
95
89
|
run();
|
|
96
90
|
});
|
|
97
|
-
} // Returns
|
|
91
|
+
} // Returns true if the URL hostname matches any patterns
|
|
98
92
|
|
|
99
93
|
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
function hostnameMatches(patterns, url) {
|
|
95
|
+
let subject = new URL(url);
|
|
96
|
+
/* istanbul ignore next: only strings are provided internally by the client proxy; core (which
|
|
97
|
+
* borrows this util) sometimes provides an array of patterns or undefined */
|
|
103
98
|
|
|
99
|
+
patterns = typeof patterns === 'string' ? patterns.split(/[\s,]+/) : [].concat(patterns);
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} = httpModuleFor(url);
|
|
109
|
-
return new Agent({
|
|
110
|
-
keepAlive: true,
|
|
111
|
-
maxSockets: 5
|
|
112
|
-
});
|
|
113
|
-
} // Returns a promise that resolves when the request is successful and rejects
|
|
114
|
-
// when a non-successful response is received. The rejected error contains
|
|
115
|
-
// response data and any received error details. Server 500 errors are retried
|
|
116
|
-
// up to 5 times at 50ms intervals.
|
|
101
|
+
for (let pattern of patterns) {
|
|
102
|
+
if (pattern === '*') return true;
|
|
103
|
+
if (!pattern) continue; // parse pattern
|
|
117
104
|
|
|
105
|
+
let {
|
|
106
|
+
groups: rule
|
|
107
|
+
} = pattern.match(/^(?<hostname>.+?)(?::(?<port>\d+))?$/); // missing a hostname or ports do not match
|
|
118
108
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
hostname
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
protocol,
|
|
133
|
-
hostname,
|
|
134
|
-
port,
|
|
135
|
-
path: pathname + search
|
|
136
|
-
};
|
|
137
|
-
return retry((resolve, reject, retry) => {
|
|
138
|
-
http.request(options).on('response', res => {
|
|
139
|
-
let status = res.statusCode;
|
|
140
|
-
let raw = '';
|
|
141
|
-
res.setEncoding('utf8');
|
|
142
|
-
res.on('data', chunk => {
|
|
143
|
-
raw += chunk;
|
|
144
|
-
});
|
|
145
|
-
res.on('end', () => {
|
|
146
|
-
let body = raw; // attempt to parse json responses
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
body = JSON.parse(raw);
|
|
150
|
-
} catch (e) {} // success
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (status >= 200 && status < 300) {
|
|
154
|
-
resolve(body);
|
|
155
|
-
} else {
|
|
156
|
-
var _body, _body$errors, _body$errors$;
|
|
157
|
-
|
|
158
|
-
// error
|
|
159
|
-
let err = Object.assign(new Error(), {
|
|
160
|
-
response: {
|
|
161
|
-
status,
|
|
162
|
-
body
|
|
163
|
-
},
|
|
164
|
-
message: ((_body = body) === null || _body === void 0 ? void 0 : (_body$errors = _body.errors) === null || _body$errors === void 0 ? void 0 : (_body$errors$ = _body$errors[0]) === null || _body$errors$ === void 0 ? void 0 : _body$errors$.detail) || `${status} ${res.statusMessage || raw}`
|
|
165
|
-
}); // retry 500s
|
|
166
|
-
|
|
167
|
-
if (status >= 500) {
|
|
168
|
-
retry(err);
|
|
169
|
-
} else {
|
|
170
|
-
reject(err);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
}).on('error', reject).end(body);
|
|
175
|
-
});
|
|
109
|
+
if (!rule.hostname || rule.port && rule.port !== subject.port) {
|
|
110
|
+
continue;
|
|
111
|
+
} // wildcards are treated the same as leading dots
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
rule.hostname = rule.hostname.replace(/^\*/, ''); // hostnames are equal or end with a wildcard rule
|
|
115
|
+
|
|
116
|
+
if (rule.hostname === subject.hostname || rule.hostname.startsWith('.') && subject.hostname.endsWith(rule.hostname)) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return false;
|
|
176
122
|
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/client",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.73",
|
|
4
4
|
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/percy/cli",
|
|
8
|
+
"directory": "packages/client"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
5
13
|
"main": "dist/index.js",
|
|
6
14
|
"files": [
|
|
7
|
-
"dist"
|
|
15
|
+
"dist",
|
|
16
|
+
"test/helpers.js"
|
|
8
17
|
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=12"
|
|
20
|
+
},
|
|
9
21
|
"scripts": {
|
|
10
|
-
"build": "
|
|
22
|
+
"build": "node ../../scripts/build",
|
|
11
23
|
"lint": "eslint --ignore-path ../../.gitignore .",
|
|
12
|
-
"test": "
|
|
13
|
-
"test:coverage": "
|
|
24
|
+
"test": "node ../../scripts/test",
|
|
25
|
+
"test:coverage": "yarn test --coverage"
|
|
14
26
|
},
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
"mocha": {
|
|
19
|
-
"require": "../../scripts/babel-register"
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@percy/env": "1.0.0-beta.73",
|
|
29
|
+
"@percy/logger": "1.0.0-beta.73"
|
|
20
30
|
},
|
|
21
31
|
"devDependencies": {
|
|
22
32
|
"mock-require": "^3.0.3"
|
|
23
33
|
},
|
|
24
|
-
"
|
|
25
|
-
"@percy/env": "^1.0.0-beta.7"
|
|
26
|
-
},
|
|
27
|
-
"gitHead": "5be796ec8f17958e93ada0b634899b945c9b0d60"
|
|
34
|
+
"gitHead": "aa8160e02bea3e04ab1d3605762f89fbe79605d4"
|
|
28
35
|
}
|
package/test/helpers.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const nock = require('nock');
|
|
2
|
+
|
|
3
|
+
const DEFAULT_REPLIES = {
|
|
4
|
+
'/builds': () => [201, {
|
|
5
|
+
data: {
|
|
6
|
+
id: '123',
|
|
7
|
+
attributes: {
|
|
8
|
+
'build-number': 1,
|
|
9
|
+
'web-url': 'https://percy.io/test/test/123'
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}],
|
|
13
|
+
|
|
14
|
+
'/builds/123/snapshots': ({ body }) => [201, {
|
|
15
|
+
data: {
|
|
16
|
+
id: '4567',
|
|
17
|
+
attributes: body.attributes,
|
|
18
|
+
relationships: {
|
|
19
|
+
'missing-resources': {
|
|
20
|
+
data: body.data.relationships.resources
|
|
21
|
+
.data.map(({ id }) => ({ id }))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}]
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mockAPI = {
|
|
29
|
+
nock: null,
|
|
30
|
+
requests: null,
|
|
31
|
+
replies: null,
|
|
32
|
+
|
|
33
|
+
start(delay = 0) {
|
|
34
|
+
nock.cleanAll();
|
|
35
|
+
nock.disableNetConnect();
|
|
36
|
+
nock.enableNetConnect('storage.googleapis.com|localhost|127.0.0.1');
|
|
37
|
+
|
|
38
|
+
let n = this.nock = nock('https://percy.io/api/v1').persist();
|
|
39
|
+
let requests = this.requests = {};
|
|
40
|
+
let replies = this.replies = {};
|
|
41
|
+
|
|
42
|
+
function intercept(_, body) {
|
|
43
|
+
let { path, headers, method } = this.req;
|
|
44
|
+
|
|
45
|
+
try { body = JSON.parse(body); } catch {}
|
|
46
|
+
path = path.replace('/api/v1', '');
|
|
47
|
+
|
|
48
|
+
let req = { body, headers, method };
|
|
49
|
+
let reply = replies[path] && (
|
|
50
|
+
replies[path].length > 1
|
|
51
|
+
? replies[path].shift()
|
|
52
|
+
: replies[path][0]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
requests[path] = requests[path] || [];
|
|
56
|
+
requests[path].push(req);
|
|
57
|
+
|
|
58
|
+
return reply ? reply(req) : (
|
|
59
|
+
DEFAULT_REPLIES[path]
|
|
60
|
+
? DEFAULT_REPLIES[path](req)
|
|
61
|
+
: [200]
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
n.get(/.*/).delay(delay).reply(intercept);
|
|
66
|
+
n.post(/.*/).delay(delay).reply(intercept);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
reply(path, handler) {
|
|
70
|
+
this.replies[path] = this.replies[path] || [];
|
|
71
|
+
this.replies[path].push(handler);
|
|
72
|
+
return this;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
cleanAll() {
|
|
76
|
+
nock.cleanAll();
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
module.exports = mockAPI;
|