@percy/core 1.0.0-beta.8 → 1.0.1

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 CHANGED
@@ -2,35 +2,71 @@
2
2
 
3
3
  The core component of Percy's CLI and SDKs that handles creating builds, discovering snapshot
4
4
  assets, uploading snapshots, and finalizing builds. Uses `@percy/client` for API communication, a
5
- Puppeteer browser for asset discovery, and starts a local API server for posting snapshots from
5
+ Chromium browser for asset discovery, and starts a local API server for posting snapshots from
6
6
  other processes.
7
7
 
8
+ - [Usage](#usage)
9
+ - [`#start()`](#start)
10
+ - [`#stop()`](#stopforce)
11
+ - [`#idle()`](#idle)
12
+ - [`#snapshot()`](#snapshotoptions)
13
+ - [Advanced](#advanced)
14
+ - [Download discovery browser on install](#download-discovery-browser-on-install)
15
+ - [Skipping discovery browser download](#skipping-discovery-browser-download)
16
+
8
17
  ## Usage
9
18
 
10
- The `Percy` class will manage a Percy build and perform asset discovery on snapshots before
11
- uploading them to Percy. It also hosts a local API server for Percy SDKs to communicate with.
19
+ A `Percy` class instance can manage a Percy build, take page snapshots, and perform snapshot asset
20
+ discovery. It also hosts a local API server for Percy SDKs to communicate with.
12
21
 
13
22
  ``` js
14
23
  import Percy from '@percy/core'
15
24
 
16
- const percy = new Percy({
17
- token: PERCY_TOKEN, // defaults to PERCY_TOKEN environment variable
18
- loglevel: 'info', // what level logs to write to console
19
- server: true, // start a local API server
20
- port: 5338, // port to start the API server at
21
- concurrency: 5, // concurrency of the #capture() method
22
- snapshot: {}, // global snapshot options (see snapshots section)
23
- discovery: { // asset discovery options
24
- allowedHostnames: [], // list of hostnames allowed to capture from
25
- networkIdleTimeout: 100, // how long before network is considered idle
26
- disableAssetCache: false, // disable discovered asset caching
27
- concurrency: 5, // asset discovery concurrency
28
- launchOptions: {} // browser launch options
29
- },
30
- ...config // additional config options accessible by SDKs
31
- })
25
+ // create a new instance
26
+ const percy = new Percy(percyOptions)
27
+
28
+ // create a new instance and start it
29
+ const percy = await Percy.start(percyOptions)
32
30
  ```
33
31
 
32
+ #### Options
33
+
34
+ - `token` — Your project's `PERCY_TOKEN` (**default** `process.env.PERCY_TOKEN`)
35
+ - `loglevel` — Logger level, one of `"info"`, `"warn"`, `"error"`, `"debug"` (**default** `"info"`)
36
+ - `server` — Controls whether an API server is created (**default** `true`)
37
+ - `port` — API server port (**default** `5338`)
38
+ - `clientInfo` — Client info sent to Percy via a user-agent string
39
+ - `environmentInfo` — Environment info also sent with the user-agent string
40
+ - `deferUploads` — Defer creating a build and uploading snapshots until later
41
+ - `skipUploads` — Skip creating a build and uploading snapshots altogether
42
+
43
+ The following options can also be defined within a Percy config file
44
+
45
+ - `snapshot` — Snapshot options applied to each snapshot
46
+ - `widths` — Widths to take screenshots at (**default** `[375, 1280]`)
47
+ - `minHeight` — Minimum screenshot height (**default** `1024`)
48
+ - `percyCSS` — Percy specific CSS to inject into the snapshot
49
+ - `enableJavaScript` — Enable JavaScript for screenshots (**default** `false`)
50
+ - `discovery` — Asset discovery options
51
+ - `allowedHostnames` — Array of allowed hostnames to capture assets from
52
+ - `disallowedHostnames` — Array of hostnames where requests will be aborted
53
+ - `requestHeaders` — Request headers used when discovering snapshot assets
54
+ - `authorization` — Basic auth `username` and `password` for protected snapshot assets
55
+ - `disableCache` — Disable asset caching (**default** `false`)
56
+ - `userAgent` — Custom user-agent string used when requesting assets
57
+ - `cookies` — Browser cookies to use when requesting assets
58
+ - `networkIdleTimeout` — Milliseconds to wait for the network to idle (**default** `100`)
59
+ - `concurrency` — Asset discovery concerrency (**default** `5`)
60
+ - `launchOptions` — Asset discovery browser launch options
61
+ - `executable` — Browser executable path (**default** `process.env.PERCY_BROWSER_EXECUTABLE`)
62
+ - `timeout` — Discovery launch timeout, in milliseconds (**default** `30000`)
63
+ - `args` — Additional browser process arguments
64
+ - `headless` — Runs the browser headlessy (**default** `true`)
65
+
66
+ Additional Percy config file options are also allowed and will override any options defined by a
67
+ local config file. These config file options are also made available to SDKs via the local API
68
+ health check endpoint.
69
+
34
70
  ### `#start()`
35
71
 
36
72
  Starting a `Percy` instance will start a local API server, start the asset discovery browser, and
@@ -39,81 +75,204 @@ create a new Percy build. If an asset discovery browser is not found, one will b
39
75
  ``` js
40
76
  await percy.start()
41
77
  // [percy] Percy has started!
42
- // [percy] Created build #1: https://percy.io/org/project/123
43
78
  ```
44
79
 
45
80
  #### API Server
46
81
 
47
82
  Starting a `Percy` instance will start a local API server unless `server` is `false`. The server can
48
- be found at `http://localhost:5338/` or at the provided `port` number. All POST requests accept a
49
- JSON body with the `application/json` content-type.
83
+ be found at `http://localhost:5338/` or at the provided `port` number.
50
84
 
51
85
  - GET `/percy/healthcheck` – Responds with information about the running instance
52
86
  - GET `/percy/dom.js` – Responds with the [`@percy/dom`](./packages/dom) library
87
+ - GET `/percy/idle` - Responds when the running instance is [idle](#idle)
53
88
  - POST `/percy/snapshot` – Calls [`#snapshot()`](#snapshotoptions) with provided snapshot options
54
- - POST `/percy/stop` - Remotely [stops](#stop) the running `Percy` instance
89
+ - POST `/percy/stop` - Remotely [stops](#stopforce) the running `Percy` instance
90
+
91
+ ### `#stop([force])`
92
+
93
+ Stopping a `Percy` instance will wait for any pending snapshots, close the asset discovery browser,
94
+ close the local API server, and finalize the current Percy build. When uploads are deferred,
95
+ stopping the instance will also trigger processing of the upload queue. When `force` is `true`,
96
+ queues are cleared and closed to prevent queued snapshots from running.
97
+
98
+ ``` js
99
+ await percy.stop()
100
+ // [percy] Processing 3 snapshots...
101
+ // [percy] Snapshot taken: Snapshot one
102
+ // [percy] Snapshot taken: Snapshot two
103
+ // [percy] Snapshot taken: Snapshot three
104
+ // [percy] Uploading 3 snapshots...
105
+ // [percy] Finalized build #1: https://percy.io/org/project/123
106
+
107
+ await percy.stop(true)
108
+ // [percy] Stopping percy...
109
+ // [percy] Finalized build #1: https://percy.io/org/project/123
110
+ ```
111
+
112
+ ### `#idle()`
113
+
114
+ This method will resolve shortly after pending snapshots and uploads have completed and no more have
115
+ started. Queued tasks are not considered pending unless they are actively running, so deferred
116
+ uploads will not be awaited on with this method.
117
+
118
+ ``` js
119
+ percy.snapshot(...);
120
+ percy.snapshot(...);
121
+ percy.snapshot(...);
122
+
123
+ await percy.idle()
124
+ // [percy] Snapshot taken: ...
125
+ // [percy] Snapshot taken: ...
126
+ // [percy] Snapshot taken: ...
127
+ ```
55
128
 
56
129
  ### `#snapshot(options)`
57
130
 
58
- Performs asset discovery for the provided DOM snapshot at widths specified here or in the instance's
59
- provided `snapshot` option. This is the primary method used by Percy SDKs to upload snapshots.
131
+ Takes one or more snapshots of a page while discovering resources to upload with the snapshot. Once
132
+ asset discovery has completed, the queued snapshot will resolve and an upload task will be queued
133
+ separately. Accepts several different syntaxes for taking snapshots in various ways.
134
+
135
+ All available syntaxes will push snapshots into the snapshot queue without the need to await on the
136
+ method directly. This method resolves after the snapshot upload is queued, but does not await on the
137
+ upload to complete.
60
138
 
61
139
  ``` js
62
140
  // snapshots can be handled concurrently, no need to await
63
141
  percy.snapshot({
64
- name: 'My Snapshot', // required name
65
- url: 'http://localhost:3000', // required url
66
- domSnapshot: domSnapshot, // required DOM string
67
- widths: [500, 1280], // widths to discover resources
68
- minHeight: 1024, // minimum height used when screenshotting
69
- percyCSS: '', // percy specific css to inject
70
- requestHeaders: {}, // asset request headers such as authorization
71
- clientInfo: '', // user-agent client info for the SDK
72
- environmentInfo: '' // user-agent environment info for the SDK
142
+ name: 'Snapshot 1',
143
+ url: 'http://localhost:3000',
144
+ domSnapshot: domSnapshot,
145
+ clientInfo: 'my-sdk',
146
+ environmentInfo: 'my-lib'
147
+ })
148
+
149
+ // without a domSnapshot, capture options will be used to take one
150
+ percy.snapshot({
151
+ name: 'Snapshot 2',
152
+ url: 'http://localhost:3000',
153
+ waitForTimeout: 1000,
154
+ waitForSelector: '.done-loading',
155
+ execute: async () => {},
156
+ additionalSnapshots: [{
157
+ name: 'Snapshot 2.1',
158
+ execute: () => {}
159
+ }]
73
160
  })
74
- ```
75
161
 
76
- ### `#capture(options)`
162
+ // alternate shorthand syntax
163
+ percy.snapshot({
164
+ baseUrl: 'http://localhost:3000',
165
+ snapshots: ['/', '/about', '/contact'],
166
+ options: {
167
+ widths: [600, 1200]
168
+ }
169
+ })
77
170
 
78
- Navigates to a URL and captures a snapshot or multiple snapshots of a page after optionally
79
- interacting with the page. Any [`#snapshot()`](#snapshotoptions) options can also be provided, with
80
- the exception of `domSnapshot`.
171
+ // gather snapshots from an external sitemap
172
+ percy.snapshot({
173
+ sitemap: 'https://example.com/sitemap.xml',
174
+ exclude: ['/blog/*']
175
+ })
81
176
 
82
- ``` js
83
- // pages can be captured concurrently, no need to await
84
- percy.capture({
85
- name: 'My Snapshot', // snapshot name
86
- url: 'http://localhost:3000/', // required page URL
87
- waitFor: selectorOrTimeout, // selector or timeout to wait for before snapshotting
88
- execute: async (page) => {}, // async page function to execute before snapshotting
89
- snapshots: [{ // additional snapshots to take on this page
90
- name: 'Second Snapshot', // additional snapshot name
91
- execute: async (page) => {}, // additional snapshot page function
92
- ...options // ...additional snapshot options
93
- }],
94
- ...options // ...other snapshot options
177
+ // start a server and take static snapshots
178
+ percy.snapshot({
179
+ serve: './public',
180
+ cleanUrls: true,
95
181
  })
96
182
  ```
97
183
 
98
- ### `#stop()`
184
+ #### Options
99
185
 
100
- Stopping a `Percy` instance will wait for any pending snapshots, close the asset discovery browser,
101
- close the local API server, and finalize the current Percy build.
186
+ When capturing a single snapshot, the snapshot URL may be provided as the only argument rather than
187
+ a snapshot options object. When providing an options object, a few alternate syntaxes are available
188
+ depending on the provided properties ([see alternate syntaxes below](#alternate-syntaxes)).
102
189
 
103
- ``` js
104
- await percy.stop()
105
- // [percy] Stopping percy...
106
- // [percy] Waiting for 1 snapshot(s) to complete
107
- // [percy] Snapshot taken: My Snapshot
108
- // [percy] Finalized build #1: https://percy.io/org/project/123
109
- // [percy] Done
110
- ```
190
+ **Common options** accepted for each snapshot:
191
+
192
+ - `url` Snapshot URL (**required**)
193
+ - `name` Snapshot name
194
+ - `domSnapshot` Snapshot DOM string
195
+ - `discovery` - Limited snapshot specific discovery options
196
+ - `allowedHostnames`, `disallowedHostnames`, `requestHeaders`, `authorization`, `disableCache`, `userAgent`
197
+
198
+ Common snapshot options are also accepted and will override instance snapshot options. [See instance
199
+ options](#options) for common snapshot and discovery options.
200
+
201
+ **Capture options** can only be provided when `domSnapshot` is missing:
202
+
203
+ - `waitForTimeout` — Milliseconds to wait before taking a snapshot
204
+ - `waitForSelector` — CSS selector to wait for before taking a snapshot
205
+ - `execute` — Function or function body to execute within the page before taking a snapshot
206
+ - `additionalSnapshots` — Array of additional sequential snapshots to take of the page
207
+ - `name` — Snapshot name (**required** if no `prefix` or `suffix`)
208
+ - `prefix` — Snapshot name prefix (**required** if no `name` or `suffix`)
209
+ - `suffix` — Snapshot name suffix (**required** if no `name` or `prefix`)
210
+ - `waitForTimeout`, `waitForSelector`, `execute` — See above
211
+
212
+ #### Alternate syntaxes
213
+
214
+ All snapshot syntaxes can be provided as items within an array. For example, a single method call
215
+ can upload multiple DOM snapshots, capture multiple external snapshots, crawl a sitemap for
216
+ snapshots, and host a local static server for snapshots.
217
+
218
+ **Shared options** accepted by all syntaxes:
219
+
220
+ - `clientInfo` — Client info to include with the build
221
+ - `environmentInfo` — Environment info to include with the build
222
+
223
+ The following alternate syntaxes may **not** be combined with snapshot options, but rather offer
224
+ alternate methods for taking multiple snapshots.
225
+
226
+ **List options** can only be provided when a top-level `snapshots` is present:
227
+
228
+ - `snapshots` — An array of snapshot URLs or snapshot options (**required**)
229
+ - `baseUrl` — The full base URL (including protocol) used when snapshot URLs only include a pathname
230
+ - `include`/`exclude` — Include and exclude matching snapshot names
231
+ - `options` — Additional options to apply to snapshots
232
+ - `include`/`exclude` — Include and exclude snapshots to apply these options to
233
+ - [Common snapshot and capture options](#options) (**excluding** `url`, `domSnapshot`)
234
+
235
+ **Sitemap options** can only be provided when a top-level `sitemap` is present:
236
+
237
+ - `sitemap` — The URL where an XML sitemap can be located (**required**)
238
+ - `include`/`exclude` — Include and exclude matching snapshot names
239
+ - `options` — Additional options to apply to snapshots
240
+ - `include`/`exclude` — Include and exclude snapshots to apply these options to
241
+ - [Common snapshot and capture options](#options) (**excluding** `url`, `domSnapshot`)
242
+
243
+ **Server options** can only be provided when a top-level `serve` is present:
244
+
245
+ - `serve` — The static directory to serve relative to the current working directory (**required**)
246
+ - `baseUrl` — The base URL to serve the directory at, starting with a forward slash (/)
247
+ - `cleanUrls` — Set to `true` to strip `.html` and `index.html` from served URLs
248
+ - `rewrites` — A source-destination map for rewriting source URLs into destination pathnames
249
+ - `snapshots` — An array of specific snapshots to take while serving the static directory
250
+ - `include`/`exclude` — Include and exclude matching snapshot names
251
+ - `options` — Additional options to apply to snapshots
252
+ - `include`/`exclude` — Include and exclude snapshots to apply these options to
253
+ - [Common snapshot and capture options](#options) (**excluding** `url`, `domSnapshot`)
111
254
 
112
255
  ## Advanced
113
256
 
114
- ### Puppeteer Executable Path
257
+ ### Download discovery browser on install
258
+
259
+ By default, the browser is only downloaded when asset discovery is started for the first time. This
260
+ is because many features of the CLI do not require a browser at all, and automatically downloading a
261
+ browser creates a much heavier footprint than needed for those features. However, if your CI caches
262
+ dependencies after the install step, the browser will not be cached and will be downloaded every
263
+ time Percy runs without it.
264
+
265
+ If the environment variable `PERCY_POSTINSTALL_BROWSER` is present and truthy, then the browser will
266
+ be downloaded after the package is installed to allow it to be cached. You can also require
267
+ `@percy/core/post-install` within another node module to trigger the browser download manually.
268
+
269
+ ### Skipping discovery browser download
115
270
 
116
- To avoid downloading the browser used for asset discovery, the local browser executable can be
117
- defined with an `executablePath` option provided within `discovery.launchOptions`. This options
118
- falls back to the `PUPPETEER_EXECUTABLE_PATH` environment variable if defined.
271
+ If your CI comes with a Chromium binary pre-installed and you wish to skip Percy's own browser
272
+ installation, you can set the respective `discovery.launchOptions.executable` config option. When
273
+ the executable at the provided path exists, the default download will be skipped and the provided
274
+ binary will be used instead. This option can also be set using the `PERCY_BROWSER_EXECUTABLE`
275
+ environment variable.
119
276
 
277
+ > **Warning!** Percy is only tested against the browser it downloads automatically. When providing a
278
+ > custom browser executable, you may experience unexpected issues.
package/dist/api.js ADDED
@@ -0,0 +1,94 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { createRequire } from 'module';
4
+ import logger from '@percy/logger';
5
+ import { getPackageJSON } from './utils.js';
6
+ import Server from './server.js'; // need require.resolve until import.meta.resolve can be transpiled
7
+
8
+ export const PERCY_DOM = createRequire(import.meta.url).resolve('@percy/dom'); // Create a Percy CLI API server instance
9
+
10
+ export function createPercyServer(percy, port) {
11
+ let pkg = getPackageJSON(import.meta.url);
12
+ return new Server({
13
+ port
14
+ }) // facilitate logger websocket connections
15
+ .websocket(ws => logger.connect(ws)) // general middleware
16
+ .route((req, res, next) => {
17
+ // treat all request bodies as json
18
+ if (req.body) try {
19
+ req.body = JSON.parse(req.body);
20
+ } catch {} // add version header
21
+
22
+ res.setHeader('Access-Control-Expose-Headers', '*, X-Percy-Core-Version');
23
+ res.setHeader('X-Percy-Core-Version', pkg.version); // return json errors
24
+
25
+ return next().catch(e => res.json(e.status ?? 500, {
26
+ build: percy.build,
27
+ error: e.message,
28
+ success: false
29
+ }));
30
+ }) // healthcheck returns basic information
31
+ .route('get', '/percy/healthcheck', (req, res) => res.json(200, {
32
+ loglevel: percy.loglevel(),
33
+ config: percy.config,
34
+ build: percy.build,
35
+ success: true
36
+ })) // get or set config options
37
+ .route(['get', 'post'], '/percy/config', async (req, res) => res.json(200, {
38
+ config: req.body ? await percy.setConfig(req.body) : percy.config,
39
+ success: true
40
+ })) // responds once idle (may take a long time)
41
+ .route('get', '/percy/idle', async (req, res) => res.json(200, {
42
+ success: await percy.idle().then(() => true)
43
+ })) // convenient @percy/dom bundle
44
+ .route('get', '/percy/dom.js', (req, res) => {
45
+ return res.file(200, PERCY_DOM);
46
+ }) // legacy agent wrapper for @percy/dom
47
+ .route('get', '/percy-agent.js', async (req, res) => {
48
+ logger('core:server').deprecated(['It looks like you’re using @percy/cli with an older SDK.', 'Please upgrade to the latest version to fix this warning.', 'See these docs for more info: https:docs.percy.io/docs/migrating-to-percy-cli'].join(' '));
49
+ let content = await fs.promises.readFile(PERCY_DOM, 'utf-8');
50
+ let wrapper = '(window.PercyAgent = class { snapshot(n, o) { return PercyDOM.serialize(o); } });';
51
+ return res.send(200, 'applicaton/javascript', content.concat(wrapper));
52
+ }) // post one or more snapshots
53
+ .route('post', '/percy/snapshot', async (req, res) => {
54
+ let snapshot = percy.snapshot(req.body);
55
+ if (!req.url.searchParams.has('async')) await snapshot;
56
+ return res.json(200, {
57
+ success: true
58
+ });
59
+ }) // stops percy at the end of the current event loop
60
+ .route('/percy/stop', (req, res) => {
61
+ setImmediate(() => percy.stop());
62
+ return res.json(200, {
63
+ success: true
64
+ });
65
+ });
66
+ } // Create a static server instance with an automatic sitemap
67
+
68
+ export function createStaticServer(options) {
69
+ let {
70
+ serve,
71
+ port,
72
+ baseUrl = '/',
73
+ ...opts
74
+ } = options;
75
+ let server = new Server({
76
+ port
77
+ }).serve(baseUrl, serve, opts); // used when generating an automatic sitemap
78
+
79
+ let toURL = Server.createRewriter( // reverse rewrites' src, dest, & order
80
+ Object.entries((options === null || options === void 0 ? void 0 : options.rewrites) ?? {}).reduce((acc, rw) => [rw.reverse(), ...acc], []), (filename, rewrite) => new URL(path.posix.join(baseUrl, // cleanUrls will trim trailing .html/index.html from paths
81
+ !options.cleanUrls ? rewrite(filename) : rewrite(filename).replace(/(\/index)?\.html$/, '')), server.address())); // include automatic sitemap route
82
+
83
+ server.route('get', '/sitemap.xml', async (req, res) => {
84
+ let {
85
+ default: glob
86
+ } = await import('fast-glob');
87
+ let files = await glob('**/*.html', {
88
+ cwd: serve,
89
+ fs
90
+ });
91
+ return res.send(200, 'application/xml', ['<?xml version="1.0" encoding="UTF-8"?>', '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">', ...files.map(name => ` <url><loc>${toURL(name)}</loc></url>`), '</urlset>'].join('\n'));
92
+ });
93
+ return server;
94
+ }