@percy/client 1.0.0-beta.9 → 1.0.0
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/package.json +26 -16
- package/dist/client.js +0 -357
- package/dist/index.js +0 -15
- package/dist/utils.js +0 -176
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
|
-
minHeight,
|
|
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/package.json
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/client",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "babel --root-mode upward src --out-dir dist",
|
|
11
|
-
"lint": "eslint --ignore-path ../../.gitignore .",
|
|
12
|
-
"test": "cross-env NODE_ENV=test mocha",
|
|
13
|
-
"test:coverage": "nyc yarn test"
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/percy/cli",
|
|
8
|
+
"directory": "packages/client"
|
|
14
9
|
},
|
|
15
10
|
"publishConfig": {
|
|
16
11
|
"access": "public"
|
|
17
12
|
},
|
|
18
|
-
"
|
|
19
|
-
"
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=14"
|
|
20
15
|
},
|
|
21
|
-
"
|
|
22
|
-
"
|
|
16
|
+
"files": [
|
|
17
|
+
"./dist",
|
|
18
|
+
"./test/helpers.js"
|
|
19
|
+
],
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./dist/index.js",
|
|
24
|
+
"./utils": "./dist/utils.js",
|
|
25
|
+
"./test/helpers": "./test/helpers.js"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "node ../../scripts/build",
|
|
29
|
+
"lint": "eslint --ignore-path ../../.gitignore .",
|
|
30
|
+
"test": "node ../../scripts/test",
|
|
31
|
+
"test:coverage": "yarn test --coverage"
|
|
23
32
|
},
|
|
24
33
|
"dependencies": {
|
|
25
|
-
"@percy/env": "
|
|
34
|
+
"@percy/env": "1.0.0",
|
|
35
|
+
"@percy/logger": "1.0.0"
|
|
26
36
|
},
|
|
27
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "6df509421a60144e4f9f5d59dc57a5675372a0b2"
|
|
28
38
|
}
|
package/dist/client.js
DELETED
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.default = void 0;
|
|
7
|
-
|
|
8
|
-
var _env = _interopRequireDefault(require("@percy/env"));
|
|
9
|
-
|
|
10
|
-
var _git = require("@percy/env/dist/git");
|
|
11
|
-
|
|
12
|
-
var _package = _interopRequireDefault(require("../package.json"));
|
|
13
|
-
|
|
14
|
-
var _utils = require("./utils");
|
|
15
|
-
|
|
16
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
|
-
|
|
18
|
-
// PercyClient is used to communicate with the Percy API to create and finalize
|
|
19
|
-
// builds and snapshot. Uses @percy/env to collect environment information used
|
|
20
|
-
// during build creation.
|
|
21
|
-
class PercyClient {
|
|
22
|
-
constructor({
|
|
23
|
-
// read or write token, defaults to PERCY_TOKEN environment variable
|
|
24
|
-
token,
|
|
25
|
-
// initial user agent info
|
|
26
|
-
clientInfo = '',
|
|
27
|
-
environmentInfo = '',
|
|
28
|
-
// versioned percy api url
|
|
29
|
-
apiUrl = 'https://percy.io/api/v1'
|
|
30
|
-
} = {}) {
|
|
31
|
-
Object.assign(this, {
|
|
32
|
-
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
|
-
}
|
|
44
|
-
});
|
|
45
|
-
} // Adds additional unique client info.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
addClientInfo(info) {
|
|
49
|
-
if (info && this.clientInfo.indexOf(info) === -1) {
|
|
50
|
-
this.clientInfo.push(info);
|
|
51
|
-
}
|
|
52
|
-
} // Adds additional unique environment info.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
addEnvironmentInfo(info) {
|
|
56
|
-
if (info && this.environmentInfo.indexOf(info) === -1) {
|
|
57
|
-
this.environmentInfo.push(info);
|
|
58
|
-
}
|
|
59
|
-
} // Stringifies client and environment info.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
userAgent() {
|
|
63
|
-
let client = [`Percy/${/\w+$/.exec(this.apiUrl)}`].concat(`${_package.default.name}/${_package.default.version}`, this.clientInfo).filter(Boolean).join(' ');
|
|
64
|
-
let environment = this.environmentInfo.concat([`node/${process.version}`, this.env.info]).filter(Boolean).join('; ');
|
|
65
|
-
return `${client} (${environment})`;
|
|
66
|
-
} // Checks for a Percy token and returns it.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
getToken() {
|
|
70
|
-
let token = this.token || this.env.token;
|
|
71
|
-
if (!token) throw new Error('Missing Percy token');
|
|
72
|
-
return token;
|
|
73
|
-
} // Returns common headers used for each request with additional
|
|
74
|
-
// headers. Throws an error when the token is missing, which is a required
|
|
75
|
-
// authorization header.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
headers(headers) {
|
|
79
|
-
return Object.assign({
|
|
80
|
-
Authorization: `Token token=${this.getToken()}`,
|
|
81
|
-
'User-Agent': this.userAgent()
|
|
82
|
-
}, headers);
|
|
83
|
-
} // Performs a GET request for an API endpoint with appropriate headers.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
get(path) {
|
|
87
|
-
return (0, _utils.request)(`${this.apiUrl}/${path}`, {
|
|
88
|
-
method: 'GET',
|
|
89
|
-
agent: this.httpAgent,
|
|
90
|
-
headers: this.headers()
|
|
91
|
-
});
|
|
92
|
-
} // Performs a POST request to a JSON API endpoint with appropriate headers.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
post(path, body = {}) {
|
|
96
|
-
return (0, _utils.request)(`${this.apiUrl}/${path}`, {
|
|
97
|
-
method: 'POST',
|
|
98
|
-
agent: this.httpAgent,
|
|
99
|
-
body: JSON.stringify(body),
|
|
100
|
-
headers: this.headers({
|
|
101
|
-
'Content-Type': 'application/vnd.api+json'
|
|
102
|
-
})
|
|
103
|
-
});
|
|
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
|
-
} // Creates a build with optional build resources. Only one build can be
|
|
118
|
-
// created at a time per instance so snapshots and build finalization can be
|
|
119
|
-
// done more seemlessly without manually tracking build ids
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
async createBuild({
|
|
123
|
-
resources = []
|
|
124
|
-
} = {}) {
|
|
125
|
-
if (this.build.id) {
|
|
126
|
-
throw new Error('This client instance has not finalized the previous build');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let body = await this.post('builds', {
|
|
130
|
-
data: {
|
|
131
|
-
type: 'builds',
|
|
132
|
-
attributes: {
|
|
133
|
-
branch: this.env.git.branch,
|
|
134
|
-
'target-branch': this.env.target.branch,
|
|
135
|
-
'target-commit-sha': this.env.target.commit,
|
|
136
|
-
'commit-sha': this.env.git.sha,
|
|
137
|
-
'commit-committed-at': this.env.git.committedAt,
|
|
138
|
-
'commit-author-name': this.env.git.authorName,
|
|
139
|
-
'commit-author-email': this.env.git.authorEmail,
|
|
140
|
-
'commit-committer-name': this.env.git.committerName,
|
|
141
|
-
'commit-committer-email': this.env.git.committerEmail,
|
|
142
|
-
'commit-message': this.env.git.message,
|
|
143
|
-
'pull-request-number': this.env.pullRequest,
|
|
144
|
-
'parallel-nonce': this.env.parallel.nonce,
|
|
145
|
-
'parallel-total-shards': this.env.parallel.total,
|
|
146
|
-
partial: this.env.partial
|
|
147
|
-
},
|
|
148
|
-
relationships: {
|
|
149
|
-
resources: {
|
|
150
|
-
data: resources.map(r => ({
|
|
151
|
-
type: 'resources',
|
|
152
|
-
id: r.sha || (0, _utils.sha256hash)(r.content),
|
|
153
|
-
attributes: {
|
|
154
|
-
'resource-url': r.url,
|
|
155
|
-
'is-root': r.root || null,
|
|
156
|
-
mimetype: r.mimetype || null
|
|
157
|
-
}
|
|
158
|
-
}))
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
this.setBuildData(body === null || body === void 0 ? void 0 : body.data);
|
|
164
|
-
return body;
|
|
165
|
-
} // Finalizes the active build. When `all` is true, `all-shards=true` is
|
|
166
|
-
// added as a query param so the API finalizes all other build shards.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
async finalizeBuild({
|
|
170
|
-
all = false
|
|
171
|
-
} = {}) {
|
|
172
|
-
if (!this.build.id) {
|
|
173
|
-
throw new Error('This client instance has no active build');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
let qs = all ? 'all-shards=true' : '';
|
|
177
|
-
let body = await this.post(`builds/${this.build.id}/finalize?${qs}`);
|
|
178
|
-
this.setBuildData();
|
|
179
|
-
return body;
|
|
180
|
-
} // Retrieves build data by id. Requires a read access token.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
async getBuild(buildId) {
|
|
184
|
-
return this.get(`builds/${buildId}`);
|
|
185
|
-
} // Retrieves project builds optionally filtered. Requires a read access token.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
async getBuilds(projectSlug, filters = {}) {
|
|
189
|
-
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
|
-
return this.get(`projects/${projectSlug}/builds?${qs}`);
|
|
191
|
-
} // Resolves when the build has finished and is no longer pending or
|
|
192
|
-
// processing. By default, will time out if no update after 10 minutes.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
waitForBuild({
|
|
196
|
-
build,
|
|
197
|
-
project,
|
|
198
|
-
commit,
|
|
199
|
-
progress,
|
|
200
|
-
timeout = 600000,
|
|
201
|
-
interval = 1000
|
|
202
|
-
}) {
|
|
203
|
-
if (commit && !project) {
|
|
204
|
-
throw new Error('Missing project for commit');
|
|
205
|
-
} else if (!commit && !build) {
|
|
206
|
-
throw new Error('Missing build ID or commit SHA');
|
|
207
|
-
} // get build data by id or project-commit combo
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
let getBuildData = async () => {
|
|
211
|
-
let sha = commit && ((0, _git.git)(`rev-parse ${commit}`) || commit);
|
|
212
|
-
let body = build ? await this.getBuild(build) : await this.getBuilds(project, {
|
|
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
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return new Promise((resolve, reject) => async function poll(last, t) {
|
|
221
|
-
try {
|
|
222
|
-
let [data, state] = await getBuildData();
|
|
223
|
-
let updated = JSON.stringify(data) !== JSON.stringify(last);
|
|
224
|
-
let pending = !state || state === 'pending' || state === 'processing'; // new data recieved
|
|
225
|
-
|
|
226
|
-
if (updated) {
|
|
227
|
-
t = Date.now(); // no new data within the timeout
|
|
228
|
-
} else if (Date.now() - t >= timeout) {
|
|
229
|
-
throw new Error('Timeout exceeded without an update');
|
|
230
|
-
} // call progress after the first update
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if ((last || pending) && updated && progress) {
|
|
234
|
-
progress(data);
|
|
235
|
-
} // not finished, poll again
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (pending) {
|
|
239
|
-
return setTimeout(poll, interval, data, t); // build finished
|
|
240
|
-
} else {
|
|
241
|
-
resolve(data);
|
|
242
|
-
}
|
|
243
|
-
} catch (err) {
|
|
244
|
-
reject(err);
|
|
245
|
-
}
|
|
246
|
-
}(null, Date.now()));
|
|
247
|
-
} // Uploads a single resource to the active build. If `filepath` is provided,
|
|
248
|
-
// `content` is read from the filesystem. The sha is optional and will be
|
|
249
|
-
// created from `content` if one is not provided.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
async uploadResource({
|
|
253
|
-
sha,
|
|
254
|
-
filepath,
|
|
255
|
-
content
|
|
256
|
-
}) {
|
|
257
|
-
if (!this.build.id) {
|
|
258
|
-
throw new Error('This client instance has no active build');
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
content = filepath ? require('fs').readFileSync(filepath) : content;
|
|
262
|
-
return this.post(`builds/${this.build.id}/resources`, {
|
|
263
|
-
data: {
|
|
264
|
-
type: 'resources',
|
|
265
|
-
id: sha || (0, _utils.sha256hash)(content),
|
|
266
|
-
attributes: {
|
|
267
|
-
'base64-content': (0, _utils.base64encode)(content)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
} // Uploads resources to the active build concurrently, two at a time.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
async uploadResources(resources) {
|
|
275
|
-
if (!this.build.id) {
|
|
276
|
-
throw new Error('This client instance has no active build');
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return (0, _utils.pool)(function* () {
|
|
280
|
-
for (let resource of resources) {
|
|
281
|
-
yield this.uploadResource(resource);
|
|
282
|
-
}
|
|
283
|
-
}, this, 2);
|
|
284
|
-
} // Creates a snapshot for the active build using the provided attributes.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
async createSnapshot({
|
|
288
|
-
name,
|
|
289
|
-
widths,
|
|
290
|
-
minHeight,
|
|
291
|
-
enableJavaScript,
|
|
292
|
-
clientInfo,
|
|
293
|
-
environmentInfo,
|
|
294
|
-
resources = []
|
|
295
|
-
} = {}) {
|
|
296
|
-
if (!this.build.id) {
|
|
297
|
-
throw new Error('This client instance has no active build');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
this.addClientInfo(clientInfo);
|
|
301
|
-
this.addEnvironmentInfo(environmentInfo);
|
|
302
|
-
return this.post(`builds/${this.build.id}/snapshots`, {
|
|
303
|
-
data: {
|
|
304
|
-
type: 'snapshots',
|
|
305
|
-
attributes: {
|
|
306
|
-
name: name || null,
|
|
307
|
-
widths: widths || null,
|
|
308
|
-
'minimum-height': minHeight || null,
|
|
309
|
-
'enable-javascript': enableJavaScript || null
|
|
310
|
-
},
|
|
311
|
-
relationships: {
|
|
312
|
-
resources: {
|
|
313
|
-
data: resources.map(r => ({
|
|
314
|
-
type: 'resources',
|
|
315
|
-
id: r.sha || (0, _utils.sha256hash)(r.content),
|
|
316
|
-
attributes: {
|
|
317
|
-
'resource-url': r.url || null,
|
|
318
|
-
'is-root': r.root || null,
|
|
319
|
-
mimetype: r.mimetype || null
|
|
320
|
-
}
|
|
321
|
-
}))
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
} // Finalizes a snapshot.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
async finalizeSnapshot(snapshotId) {
|
|
330
|
-
return this.post(`snapshots/${snapshotId}/finalize`);
|
|
331
|
-
} // Convenience method for creating a snapshot for the active build, uploading
|
|
332
|
-
// missing resources for the snapshot, and finalizing the snapshot.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
async sendSnapshot(options) {
|
|
336
|
-
var _data$relationships, _data$relationships$m;
|
|
337
|
-
|
|
338
|
-
let {
|
|
339
|
-
data
|
|
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;
|
|
342
|
-
|
|
343
|
-
if (missing === null || missing === void 0 ? void 0 : missing.length) {
|
|
344
|
-
let resources = options.resources.reduce((acc, r) => Object.assign(acc, {
|
|
345
|
-
[r.sha]: r
|
|
346
|
-
}), {});
|
|
347
|
-
await this.uploadResources(missing.map(({
|
|
348
|
-
id
|
|
349
|
-
}) => resources[id]));
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
await this.finalizeSnapshot(data.id);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
exports.default = PercyClient;
|
package/dist/index.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
Object.defineProperty(exports, "default", {
|
|
7
|
-
enumerable: true,
|
|
8
|
-
get: function () {
|
|
9
|
-
return _client.default;
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
var _client = _interopRequireDefault(require("./client"));
|
|
14
|
-
|
|
15
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
package/dist/utils.js
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.sha256hash = sha256hash;
|
|
7
|
-
exports.base64encode = base64encode;
|
|
8
|
-
exports.pool = pool;
|
|
9
|
-
exports.httpAgentFor = httpAgentFor;
|
|
10
|
-
exports.request = request;
|
|
11
|
-
|
|
12
|
-
var _crypto = _interopRequireDefault(require("crypto"));
|
|
13
|
-
|
|
14
|
-
var _url = require("url");
|
|
15
|
-
|
|
16
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
|
-
|
|
18
|
-
// Returns a sha256 hash of a string.
|
|
19
|
-
function sha256hash(content) {
|
|
20
|
-
return _crypto.default.createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
21
|
-
} // Returns a base64 encoding of a string or buffer.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function base64encode(content) {
|
|
25
|
-
return Buffer.from(content).toString('base64');
|
|
26
|
-
} // Creates a concurrent pool of promises created by the given generator.
|
|
27
|
-
// Resolves when the generator's final promise resolves and rejects when any
|
|
28
|
-
// generated promise rejects.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
function pool(generator, context, concurrency) {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
let iterator = generator.call(context);
|
|
34
|
-
let queue = 0;
|
|
35
|
-
let ret = [];
|
|
36
|
-
let err; // generates concurrent promises
|
|
37
|
-
|
|
38
|
-
let proceed = () => {
|
|
39
|
-
while (queue < concurrency) {
|
|
40
|
-
let {
|
|
41
|
-
done,
|
|
42
|
-
value: promise
|
|
43
|
-
} = iterator.next();
|
|
44
|
-
|
|
45
|
-
if (done || err) {
|
|
46
|
-
if (!queue && err) reject(err);
|
|
47
|
-
if (!queue) resolve(ret);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
queue++;
|
|
52
|
-
promise.then(value => {
|
|
53
|
-
queue--;
|
|
54
|
-
ret.push(value);
|
|
55
|
-
proceed();
|
|
56
|
-
}).catch(error => {
|
|
57
|
-
queue--;
|
|
58
|
-
err = error;
|
|
59
|
-
proceed();
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}; // start generating promises
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
proceed();
|
|
66
|
-
});
|
|
67
|
-
} // Returns a promise that resolves or rejects when the provided function calls
|
|
68
|
-
// `resolve` or `reject` respectively. The third function argument, `retry`,
|
|
69
|
-
// will recursively call the function at the specified interval until retries
|
|
70
|
-
// are exhausted, at which point the promise will reject with the last error
|
|
71
|
-
// passed to `retry`.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
function retry(fn, {
|
|
75
|
-
retries = 5,
|
|
76
|
-
interval = 50
|
|
77
|
-
} = {}) {
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
79
|
-
// run the function, decrement retries
|
|
80
|
-
let run = () => {
|
|
81
|
-
fn(resolve, reject, retry);
|
|
82
|
-
retries--;
|
|
83
|
-
}; // wait an interval to try again or reject with the error
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
let retry = err => {
|
|
87
|
-
if (retries) {
|
|
88
|
-
setTimeout(run, interval);
|
|
89
|
-
} else {
|
|
90
|
-
reject(err);
|
|
91
|
-
}
|
|
92
|
-
}; // start trying
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
run();
|
|
96
|
-
});
|
|
97
|
-
} // Returns the appropriate http or https module for a given URL.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
function httpModuleFor(url) {
|
|
101
|
-
return url.match(/^https:\/\//) ? require('https') : require('http');
|
|
102
|
-
} // Returns the appropriate http or https Agent instance for a given URL.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
function httpAgentFor(url) {
|
|
106
|
-
let {
|
|
107
|
-
Agent
|
|
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.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
function request(url, {
|
|
120
|
-
body,
|
|
121
|
-
...options
|
|
122
|
-
}) {
|
|
123
|
-
let http = httpModuleFor(url);
|
|
124
|
-
let {
|
|
125
|
-
protocol,
|
|
126
|
-
hostname,
|
|
127
|
-
port,
|
|
128
|
-
pathname,
|
|
129
|
-
search
|
|
130
|
-
} = new _url.URL(url);
|
|
131
|
-
options = { ...options,
|
|
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
|
-
});
|
|
176
|
-
}
|