@percy/core 1.0.0 → 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.
@@ -0,0 +1,433 @@
1
+ import logger from '@percy/logger';
2
+ import PercyConfig from '@percy/config';
3
+ import micromatch from 'micromatch';
4
+ import { configSchema } from './config.js';
5
+ import { request, hostnameMatches, createRootResource, createPercyCSSResource, createLogResource } from './utils.js'; // Throw a better error message for missing or invalid urls
6
+
7
+ export function validURL(url, base) {
8
+ if (!url) {
9
+ throw new Error('Missing required URL for snapshot');
10
+ }
11
+
12
+ try {
13
+ return new URL(url, base);
14
+ } catch (e) {
15
+ throw new Error(`Invalid snapshot URL: ${e.input}`);
16
+ }
17
+ } // used to deserialize regular expression strings
18
+
19
+ const RE_REGEXP = /^\/(.+)\/(\w+)?$/; // Returns true or false if a snapshot matches the provided include and exclude predicates. A
20
+ // predicate can be an array of predicates, a regular expression, a glob pattern, or a function.
21
+
22
+ export function snapshotMatches(snapshot, include, exclude) {
23
+ var _include, _include2;
24
+
25
+ // support an options object as the second argument
26
+ if ((_include = include) !== null && _include !== void 0 && _include.include || (_include2 = include) !== null && _include2 !== void 0 && _include2.exclude) ({
27
+ include,
28
+ exclude
29
+ } = include); // recursive predicate test function
30
+
31
+ let test = (predicate, fallback) => {
32
+ if (predicate && typeof predicate === 'string') {
33
+ // snapshot name matches exactly or matches a glob
34
+ let result = snapshot.name === predicate || micromatch.isMatch(snapshot.name, predicate, {
35
+ basename: !predicate.startsWith('/')
36
+ }); // snapshot might match a string-based regexp pattern
37
+
38
+ if (!result) {
39
+ try {
40
+ let [, parsed = predicate, flags] = RE_REGEXP.exec(predicate) || [];
41
+ result = new RegExp(parsed, flags).test(snapshot.name);
42
+ } catch {}
43
+ }
44
+
45
+ return result;
46
+ } else if (predicate instanceof RegExp) {
47
+ // snapshot matches a regular expression
48
+ return predicate.test(snapshot.name);
49
+ } else if (typeof predicate === 'function') {
50
+ // advanced matching
51
+ return predicate(snapshot);
52
+ } else if (Array.isArray(predicate) && predicate.length) {
53
+ // array of predicates
54
+ return predicate.some(p => test(p));
55
+ } else {
56
+ // default fallback
57
+ return fallback;
58
+ }
59
+ }; // nothing to match, return true
60
+
61
+
62
+ if (!include && !exclude) return true; // not excluded or explicitly included
63
+
64
+ return !test(exclude, false) && test(include, true);
65
+ } // Accepts an array of snapshots to filter and map with matching options.
66
+
67
+ export function mapSnapshotOptions(percy, snapshots, config) {
68
+ if (!(snapshots !== null && snapshots !== void 0 && snapshots.length)) return []; // reduce options into a single function
69
+
70
+ let applyOptions = [].concat((config === null || config === void 0 ? void 0 : config.options) || []).reduceRight((next, {
71
+ include,
72
+ exclude,
73
+ ...opts
74
+ }) => snap => next( // assign additional options to included snaphots
75
+ snapshotMatches(snap, include, exclude) ? Object.assign(snap, opts) : snap), s => getSnapshotConfig(percy, s)); // reduce snapshots with overrides
76
+
77
+ return snapshots.reduce((acc, snapshot) => {
78
+ var _snapshot;
79
+
80
+ // transform snapshot URL shorthand into an object
81
+ if (typeof snapshot === 'string') snapshot = {
82
+ url: snapshot
83
+ }; // normalize the snapshot url and use it for the default name
84
+
85
+ let url = validURL(snapshot.url, config === null || config === void 0 ? void 0 : config.baseUrl);
86
+ (_snapshot = snapshot).name || (_snapshot.name = `${url.pathname}${url.search}${url.hash}`);
87
+ snapshot.url = url.href; // use the snapshot when matching include/exclude
88
+
89
+ if (snapshotMatches(snapshot, config)) {
90
+ acc.push(applyOptions(snapshot));
91
+ }
92
+
93
+ return acc;
94
+ }, []);
95
+ } // Returns an array of derived snapshot options
96
+
97
+ export async function gatherSnapshots(percy, options) {
98
+ let {
99
+ baseUrl,
100
+ snapshots
101
+ } = options;
102
+ if ('url' in options) snapshots = [options];
103
+ if ('sitemap' in options) snapshots = await getSitemapSnapshots(options); // validate evaluated snapshots
104
+
105
+ if (typeof snapshots === 'function') {
106
+ ({
107
+ snapshots
108
+ } = validateSnapshotOptions({
109
+ baseUrl,
110
+ snapshots: await snapshots(baseUrl)
111
+ }));
112
+ } // map snapshots with snapshot options
113
+
114
+
115
+ snapshots = mapSnapshotOptions(percy, snapshots, options);
116
+ if (!snapshots.length) throw new Error('No snapshots found');
117
+ return snapshots;
118
+ } // Validates and migrates snapshot options against the correct schema based on provided
119
+ // properties. Eagerly throws an error when missing a URL for any snapshot, and warns about all
120
+ // other invalid options which are also scrubbed from the returned migrated options.
121
+
122
+ export function validateSnapshotOptions(options) {
123
+ let schema; // decide which schema to validate against
124
+
125
+ if ('domSnapshot' in options) {
126
+ schema = '/snapshot/dom';
127
+ } else if ('url' in options) {
128
+ schema = '/snapshot';
129
+ } else if ('sitemap' in options) {
130
+ schema = '/snapshot/sitemap';
131
+ } else if ('serve' in options) {
132
+ schema = '/snapshot/server';
133
+ } else if ('snapshots' in options) {
134
+ schema = '/snapshot/list';
135
+ } else {
136
+ schema = '/snapshot';
137
+ }
138
+
139
+ let {
140
+ // migrate and remove certain properties from validating
141
+ clientInfo,
142
+ environmentInfo,
143
+ snapshots,
144
+ ...migrated
145
+ } = PercyConfig.migrate(options, schema); // gather info for validating individual snapshot URLs
146
+
147
+ let isSnapshot = schema === '/snapshot/dom' || schema === '/snapshot';
148
+ let baseUrl = schema === '/snapshot/server' ? 'http://localhost' : options.baseUrl;
149
+ let snaps = isSnapshot ? [migrated] : Array.isArray(snapshots) ? snapshots : [];
150
+
151
+ for (let snap of snaps) validURL(typeof snap === 'string' ? snap : snap.url, baseUrl); // add back snapshots before validating and scrubbing; function snapshots are validated later
152
+
153
+
154
+ if (snapshots) migrated.snapshots = typeof snapshots === 'function' ? [] : snapshots;
155
+ let errors = PercyConfig.validate(migrated, schema);
156
+
157
+ if (errors) {
158
+ // warn on validation errors
159
+ let log = logger('core:snapshot');
160
+ log.warn('Invalid snapshot options:');
161
+
162
+ for (let e of errors) log.warn(`- ${e.path}: ${e.message}`);
163
+ } // add back the snapshots function if there was one
164
+
165
+
166
+ if (typeof snapshots === 'function') migrated.snapshots = snapshots; // add back an empty array if all server snapshots were scrubbed
167
+
168
+ if ('serve' in options && 'snapshots' in options) migrated.snapshots ?? (migrated.snapshots = []);
169
+ return {
170
+ clientInfo,
171
+ environmentInfo,
172
+ ...migrated
173
+ };
174
+ } // Fetches a sitemap and parses it into a list of URLs for taking snapshots. Duplicate URLs,
175
+ // including a trailing slash, are removed from the resulting list.
176
+
177
+ export async function getSitemapSnapshots(options) {
178
+ return request(options.sitemap, (body, res) => {
179
+ // validate sitemap content-type
180
+ let [contentType] = res.headers['content-type'].split(';');
181
+
182
+ if (!/^(application|text)\/xml$/.test(contentType)) {
183
+ throw new Error('The sitemap must be an XML document, ' + `but the content-type was "${contentType}"`);
184
+ } // parse XML content into a list of URLs
185
+
186
+
187
+ let urls = body.match(/(?<=<loc>)(.*)(?=<\/loc>)/ig) ?? []; // filter out duplicate URLs that differ by a trailing slash
188
+
189
+ return urls.filter((url, i) => {
190
+ let match = urls.indexOf(url.replace(/\/$/, ''));
191
+ return match === -1 || match === i;
192
+ });
193
+ });
194
+ } // Return snapshot options merged with defaults and global options.
195
+
196
+ export function getSnapshotConfig(percy, options) {
197
+ return PercyConfig.merge([{
198
+ widths: configSchema.snapshot.properties.widths.default,
199
+ discovery: {
200
+ allowedHostnames: [validURL(options.url).hostname]
201
+ },
202
+ meta: {
203
+ snapshot: {
204
+ name: options.name
205
+ },
206
+ build: percy.build
207
+ }
208
+ }, percy.config.snapshot, {
209
+ // only specific discovery options are used per-snapshot
210
+ discovery: {
211
+ allowedHostnames: percy.config.discovery.allowedHostnames,
212
+ disallowedHostnames: percy.config.discovery.disallowedHostnames,
213
+ networkIdleTimeout: percy.config.discovery.networkIdleTimeout,
214
+ requestHeaders: percy.config.discovery.requestHeaders,
215
+ authorization: percy.config.discovery.authorization,
216
+ disableCache: percy.config.discovery.disableCache,
217
+ userAgent: percy.config.discovery.userAgent
218
+ }
219
+ }, options], (path, prev, next) => {
220
+ var _next;
221
+
222
+ switch (path.map(k => k.toString()).join('.')) {
223
+ case 'widths':
224
+ // dedup, sort, and override widths when not empty
225
+ return [path, (_next = next) !== null && _next !== void 0 && _next.length ? Array.from(new Set(next)).sort((a, b) => a - b) : prev];
226
+
227
+ case 'percyCSS':
228
+ // concatenate percy css
229
+ return [path, [prev, next].filter(Boolean).join('\n')];
230
+
231
+ case 'execute':
232
+ // shorthand for execute.beforeSnapshot
233
+ return Array.isArray(next) || typeof next !== 'object' ? [path.concat('beforeSnapshot'), next] : [path];
234
+
235
+ case 'discovery.disallowedHostnames':
236
+ // prevent disallowing the root hostname
237
+ return [path, (prev ?? []).concat(next).filter(h => !hostnameMatches(h, options.url))];
238
+ } // ensure additional snapshots have complete names
239
+
240
+
241
+ if (path[0] === 'additionalSnapshots' && path.length === 2) {
242
+ let {
243
+ prefix = '',
244
+ suffix = '',
245
+ ...n
246
+ } = next;
247
+ next = {
248
+ name: `${prefix}${options.name}${suffix}`,
249
+ ...n
250
+ };
251
+ return [path, next];
252
+ }
253
+ });
254
+ } // Returns a complete and valid snapshot config object and logs verbose debug logs detailing various
255
+ // snapshot options. When `showInfo` is true, specific messages will be logged as info logs rather
256
+ // than debug logs.
257
+
258
+ function debugSnapshotConfig(snapshot, showInfo) {
259
+ let log = logger('core:snapshot'); // log snapshot info
260
+
261
+ log.debug('---------', snapshot.meta);
262
+ if (showInfo) log.info(`Snapshot found: ${snapshot.name}`, snapshot.meta);else log.debug(`Handling snapshot: ${snapshot.name}`, snapshot.meta); // will log debug info for an object property if its value is defined
263
+
264
+ let debugProp = (obj, prop, format = String) => {
265
+ let val = prop.split('.').reduce((o, k) => o === null || o === void 0 ? void 0 : o[k], obj);
266
+
267
+ if (val != null) {
268
+ // join formatted array values with a space
269
+ val = [].concat(val).map(format).join(', ');
270
+ log.debug(`- ${prop}: ${val}`, snapshot.meta);
271
+ }
272
+ };
273
+
274
+ debugProp(snapshot, 'url');
275
+ debugProp(snapshot, 'widths', v => `${v}px`);
276
+ debugProp(snapshot, 'minHeight', v => `${v}px`);
277
+ debugProp(snapshot, 'enableJavaScript');
278
+ debugProp(snapshot, 'waitForTimeout');
279
+ debugProp(snapshot, 'waitForSelector');
280
+ debugProp(snapshot, 'execute.afterNavigation');
281
+ debugProp(snapshot, 'execute.beforeResize');
282
+ debugProp(snapshot, 'execute.afterResize');
283
+ debugProp(snapshot, 'execute.beforeSnapshot');
284
+ debugProp(snapshot, 'discovery.allowedHostnames');
285
+ debugProp(snapshot, 'discovery.disallowedHostnames');
286
+ debugProp(snapshot, 'discovery.requestHeaders', JSON.stringify);
287
+ debugProp(snapshot, 'discovery.authorization', JSON.stringify);
288
+ debugProp(snapshot, 'discovery.disableCache');
289
+ debugProp(snapshot, 'discovery.userAgent');
290
+ debugProp(snapshot, 'clientInfo');
291
+ debugProp(snapshot, 'environmentInfo');
292
+ debugProp(snapshot, 'domSnapshot', Boolean);
293
+
294
+ for (let added of snapshot.additionalSnapshots || []) {
295
+ if (showInfo) log.info(`Snapshot found: ${added.name}`, snapshot.meta);else log.debug(`Additional snapshot: ${added.name}`, snapshot.meta);
296
+ debugProp(added, 'waitForTimeout');
297
+ debugProp(added, 'waitForSelector');
298
+ debugProp(added, 'execute');
299
+ }
300
+ } // Calls the provided callback with additional resources
301
+
302
+
303
+ function handleSnapshotResources(snapshot, map, callback) {
304
+ let resources = [...map.values()]; // sort the root resource first
305
+
306
+ let [root] = resources.splice(resources.findIndex(r => r.root), 1);
307
+ resources.unshift(root); // inject Percy CSS
308
+
309
+ if (snapshot.percyCSS) {
310
+ let css = createPercyCSSResource(root.url, snapshot.percyCSS);
311
+ resources.push(css); // replace root contents and associated properties
312
+
313
+ Object.assign(root, createRootResource(root.url, root.content.replace(/(<\/body>)(?!.*\1)/is, `<link data-percy-specific-css rel="stylesheet" href="${css.pathname}"/>` + '$&')));
314
+ } // include associated snapshot logs matched by meta information
315
+
316
+
317
+ resources.push(createLogResource(logger.query(log => {
318
+ var _log$meta$snapshot;
319
+
320
+ return ((_log$meta$snapshot = log.meta.snapshot) === null || _log$meta$snapshot === void 0 ? void 0 : _log$meta$snapshot.name) === snapshot.meta.snapshot.name;
321
+ })));
322
+ return callback(snapshot, resources);
323
+ } // Wait for a page's asset discovery network to idle
324
+
325
+
326
+ function waitForDiscoveryNetworkIdle(page, options) {
327
+ let {
328
+ allowedHostnames,
329
+ networkIdleTimeout
330
+ } = options;
331
+
332
+ let filter = r => hostnameMatches(allowedHostnames, r.url);
333
+
334
+ return page.network.idle(filter, networkIdleTimeout);
335
+ } // Used to cache resources across core instances
336
+
337
+
338
+ const RESOURCE_CACHE_KEY = Symbol('resource-cache'); // Discovers resources for a snapshot using a browser page to intercept requests. The callback
339
+ // function will be called with the snapshot name (for additional snapshots) and an array of
340
+ // discovered resources. When additional snapshots are provided, the callback will be called once
341
+ // for each snapshot.
342
+
343
+ export async function* discoverSnapshotResources(percy, snapshot, callback) {
344
+ debugSnapshotConfig(snapshot, percy.dryRun); // when dry-running, invoke the callback for each snapshot and immediately return
345
+
346
+ let allSnapshots = [snapshot, ...(snapshot.additionalSnapshots || [])];
347
+ if (percy.dryRun) return allSnapshots.map(s => callback(s)); // keep a global resource cache across snapshots
348
+
349
+ let cache = percy[RESOURCE_CACHE_KEY] || (percy[RESOURCE_CACHE_KEY] = new Map()); // copy widths to prevent mutation later
350
+
351
+ let widths = snapshot.widths.slice(); // preload the root resource for existing dom snapshots
352
+
353
+ let resources = new Map(snapshot.domSnapshot && [createRootResource(snapshot.url, snapshot.domSnapshot)].map(resource => [resource.url, resource])); // open a new browser page
354
+
355
+ let page = yield percy.browser.page({
356
+ enableJavaScript: snapshot.enableJavaScript ?? !snapshot.domSnapshot,
357
+ networkIdleTimeout: snapshot.discovery.networkIdleTimeout,
358
+ requestHeaders: snapshot.discovery.requestHeaders,
359
+ authorization: snapshot.discovery.authorization,
360
+ userAgent: snapshot.discovery.userAgent,
361
+ meta: snapshot.meta,
362
+ // enable network inteception
363
+ intercept: {
364
+ enableJavaScript: snapshot.enableJavaScript,
365
+ disableCache: snapshot.discovery.disableCache,
366
+ allowedHostnames: snapshot.discovery.allowedHostnames,
367
+ disallowedHostnames: snapshot.discovery.disallowedHostnames,
368
+ getResource: u => resources.get(u) || cache.get(u),
369
+ saveResource: r => resources.set(r.url, r) && cache.set(r.url, r)
370
+ }
371
+ });
372
+
373
+ try {
374
+ var _snapshot$execute;
375
+
376
+ // set the initial page size
377
+ yield page.resize({
378
+ width: widths.shift(),
379
+ height: snapshot.minHeight
380
+ }); // navigate to the url
381
+
382
+ yield page.goto(snapshot.url);
383
+ yield page.evaluate((_snapshot$execute = snapshot.execute) === null || _snapshot$execute === void 0 ? void 0 : _snapshot$execute.afterNavigation); // trigger resize events for other widths
384
+
385
+ for (let width of widths) {
386
+ var _snapshot$execute2, _snapshot$execute3;
387
+
388
+ yield page.evaluate((_snapshot$execute2 = snapshot.execute) === null || _snapshot$execute2 === void 0 ? void 0 : _snapshot$execute2.beforeResize);
389
+ yield waitForDiscoveryNetworkIdle(page, snapshot.discovery);
390
+ yield page.resize({
391
+ width,
392
+ height: snapshot.minHeight
393
+ });
394
+ yield page.evaluate((_snapshot$execute3 = snapshot.execute) === null || _snapshot$execute3 === void 0 ? void 0 : _snapshot$execute3.afterResize);
395
+ }
396
+
397
+ if (snapshot.domSnapshot) {
398
+ // ensure discovery has finished and handle resources
399
+ yield waitForDiscoveryNetworkIdle(page, snapshot.discovery);
400
+ handleSnapshotResources(snapshot, resources, callback);
401
+ } else {
402
+ let {
403
+ enableJavaScript
404
+ } = snapshot; // capture snapshots sequentially
405
+
406
+ for (let snap of allSnapshots) {
407
+ // will wait for timeouts, selectors, and additional network activity
408
+ let {
409
+ url,
410
+ dom
411
+ } = yield page.snapshot({
412
+ enableJavaScript,
413
+ ...snap
414
+ });
415
+ let root = createRootResource(url, dom); // use the normalized root url to prevent duplicates
416
+
417
+ resources.set(root.url, root); // shallow merge with root snapshot options
418
+
419
+ handleSnapshotResources({ ...snapshot,
420
+ ...snap
421
+ }, resources, callback); // remove the previously captured dom snapshot
422
+
423
+ resources.delete(root.url);
424
+ }
425
+ } // page clean up
426
+
427
+
428
+ await page.close();
429
+ } catch (error) {
430
+ await page.close();
431
+ throw error;
432
+ }
433
+ }
package/dist/utils.js ADDED
@@ -0,0 +1,127 @@
1
+ import { sha256hash } from '@percy/client/utils';
2
+ export { request, getPackageJSON, hostnameMatches } from '@percy/client/utils'; // Returns the hostname portion of a URL.
3
+
4
+ export function hostname(url) {
5
+ return new URL(url).hostname;
6
+ } // Normalizes a URL by stripping hashes to ensure unique resources.
7
+
8
+ export function normalizeURL(url) {
9
+ let {
10
+ protocol,
11
+ host,
12
+ pathname,
13
+ search
14
+ } = new URL(url);
15
+ return `${protocol}//${host}${pathname}${search}`;
16
+ } // Creates a local resource object containing the resource URL, mimetype, content, sha, and any
17
+ // other additional resources attributes.
18
+
19
+ export function createResource(url, content, mimetype, attrs) {
20
+ return { ...attrs,
21
+ sha: sha256hash(content),
22
+ mimetype,
23
+ content,
24
+ url
25
+ };
26
+ } // Creates a root resource object with an additional `root: true` property. The URL is normalized
27
+ // here as a convenience since root resources are usually created outside of asset discovery.
28
+
29
+ export function createRootResource(url, content) {
30
+ return createResource(normalizeURL(url), content, 'text/html', {
31
+ root: true
32
+ });
33
+ } // Creates a Percy CSS resource object.
34
+
35
+ export function createPercyCSSResource(url, css) {
36
+ let {
37
+ href,
38
+ pathname
39
+ } = new URL(`/percy-specific.${Date.now()}.css`, url);
40
+ return createResource(href, css, 'text/css', {
41
+ pathname
42
+ });
43
+ } // Creates a log resource object.
44
+
45
+ export function createLogResource(logs) {
46
+ return createResource(`/percy.${Date.now()}.log`, JSON.stringify(logs), 'text/plain');
47
+ } // Creates a thennable, cancelable, generator instance
48
+
49
+ export function generatePromise(gen) {
50
+ var _gen, _gen2;
51
+
52
+ // ensure a generator is provided
53
+ if (typeof gen === 'function') gen = gen();
54
+ if (typeof ((_gen = gen) === null || _gen === void 0 ? void 0 : _gen.then) === 'function') return gen;
55
+ if (typeof ((_gen2 = gen) === null || _gen2 === void 0 ? void 0 : _gen2.next) !== 'function' || !(typeof gen[Symbol.iterator] === 'function' || typeof gen[Symbol.asyncIterator] === 'function')) return Promise.resolve(gen); // used to trigger cancelation
56
+
57
+ class Canceled extends Error {
58
+ name = 'Canceled';
59
+ canceled = true;
60
+ } // recursively runs the generator, maybe throwing an error when canceled
61
+
62
+
63
+ let handleNext = async (g, last) => {
64
+ let canceled = g.cancel.triggered;
65
+ let {
66
+ done,
67
+ value
68
+ } = canceled ? await g.throw(canceled) : await g.next(last);
69
+ if (canceled) delete g.cancel.triggered;
70
+ return done ? value : handleNext(g, value);
71
+ }; // handle cancelation errors by calling any cancel handlers
72
+
73
+
74
+ let cancelable = async function* () {
75
+ try {
76
+ return yield* gen;
77
+ } catch (error) {
78
+ if (error.canceled) {
79
+ let cancelers = cancelable.cancelers || [];
80
+
81
+ for (let c of cancelers) await c(error);
82
+ }
83
+
84
+ throw error;
85
+ }
86
+ }(); // augment the cancelable generator with promise-like and cancel methods
87
+
88
+
89
+ return Object.assign(cancelable, {
90
+ run: () => cancelable.promise || (cancelable.promise = handleNext(cancelable)),
91
+ then: (resolve, reject) => cancelable.run().then(resolve, reject),
92
+ catch: reject => cancelable.run().catch(reject),
93
+ cancel: message => {
94
+ cancelable.cancel.triggered = new Canceled(message);
95
+ return cancelable;
96
+ },
97
+ canceled: handler => {
98
+ (cancelable.cancelers || (cancelable.cancelers = [])).push(handler);
99
+ return cancelable;
100
+ }
101
+ });
102
+ } // Resolves when the predicate function returns true within the timeout. If an idle option is
103
+ // provided, the predicate will be checked again before resolving, after the idle period. The poll
104
+ // option determines how often the predicate check will be run.
105
+
106
+ export function waitFor(predicate, options) {
107
+ let {
108
+ poll = 10,
109
+ timeout,
110
+ idle
111
+ } = Number.isInteger(options) ? {
112
+ timeout: options
113
+ } : options || {};
114
+ return generatePromise(async function* check(start, done) {
115
+ while (true) {
116
+ if (timeout && Date.now() - start >= timeout) {
117
+ throw new Error(`Timeout of ${timeout}ms exceeded.`);
118
+ } else if (!predicate()) {
119
+ yield new Promise(r => setTimeout(r, poll, done = false));
120
+ } else if (idle && !done) {
121
+ yield new Promise(r => setTimeout(r, idle, done = true));
122
+ } else {
123
+ return;
124
+ }
125
+ }
126
+ }(Date.now()));
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,10 +14,10 @@
14
14
  "node": ">=14"
15
15
  },
16
16
  "files": [
17
- "./dist",
18
- "./post-install.js",
19
- "./types/index.d.ts",
20
- "./test/helpers/server.js"
17
+ "dist",
18
+ "post-install.js",
19
+ "types/index.d.ts",
20
+ "test/helpers/server.js"
21
21
  ],
22
22
  "main": "./dist/index.js",
23
23
  "types": "types/index.d.ts",
@@ -38,10 +38,10 @@
38
38
  "test:types": "tsd"
39
39
  },
40
40
  "dependencies": {
41
- "@percy/client": "1.0.0",
42
- "@percy/config": "1.0.0",
43
- "@percy/dom": "1.0.0",
44
- "@percy/logger": "1.0.0",
41
+ "@percy/client": "1.0.1",
42
+ "@percy/config": "1.0.1",
43
+ "@percy/dom": "1.0.1",
44
+ "@percy/logger": "1.0.1",
45
45
  "content-disposition": "^0.5.4",
46
46
  "cross-spawn": "^7.0.3",
47
47
  "extract-zip": "^2.0.1",
@@ -52,5 +52,5 @@
52
52
  "rimraf": "^3.0.2",
53
53
  "ws": "^8.0.0"
54
54
  },
55
- "gitHead": "6df509421a60144e4f9f5d59dc57a5675372a0b2"
55
+ "gitHead": "38917e6027299d6cd86008e2ccd005d90bbf89c0"
56
56
  }
@@ -0,0 +1,20 @@
1
+ import fs from 'fs';
2
+
3
+ try {
4
+ if (process.env.PERCY_POSTINSTALL_BROWSER) {
5
+ // Automatically download and install Chromium if PERCY_POSTINSTALL_BROWSER is set
6
+ await import('./dist/install.js').then(install => install.chromium());
7
+ } else if (!process.send && fs.existsSync('./src')) {
8
+ // In development, fork this script with the development loader and always install
9
+ await import('child_process').then(cp => cp.fork('./post-install.js', {
10
+ execArgv: ['--no-warnings', '--loader=../../scripts/loader.js'],
11
+ env: { PERCY_POSTINSTALL_BROWSER: true }
12
+ }));
13
+ }
14
+ } catch (error) {
15
+ const { logger } = await import('@percy/logger');
16
+ const log = logger('core:post-install');
17
+
18
+ log.error('Encountered an error while installing Chromium');
19
+ log.error(error);
20
+ }
@@ -0,0 +1,33 @@
1
+ // aliased to src for coverage during tests without needing to compile this file
2
+ import Server from '../../dist/server.js';
3
+
4
+ export function createTestServer({ default: defaultReply, ...replies }, port = 8000) {
5
+ let server = new Server();
6
+
7
+ // alternate route handling
8
+ let handleReply = reply => async (req, res) => {
9
+ let [status, headers, body] = typeof reply === 'function' ? await reply(req) : reply;
10
+ if (!Buffer.isBuffer(body) && typeof body !== 'string') body = JSON.stringify(body);
11
+ return res.send(status, headers, body);
12
+ };
13
+
14
+ // map replies to alternate route handlers
15
+ server.reply = (p, reply) => (replies[p] = handleReply(reply), null);
16
+ for (let [p, reply] of Object.entries(replies)) server.reply(p, reply);
17
+ if (defaultReply) defaultReply = handleReply(defaultReply);
18
+
19
+ // track requests and route replies
20
+ server.requests = [];
21
+ server.route(async (req, res, next) => {
22
+ let pathname = req.url.pathname;
23
+ if (req.url.search) pathname += req.url.search;
24
+ server.requests.push(req.body ? [pathname, req.body] : [pathname]);
25
+ let reply = replies[req.url.pathname] || defaultReply;
26
+ return reply ? await reply(req, res) : next();
27
+ });
28
+
29
+ // automatically listen
30
+ return server.listen(port);
31
+ };
32
+
33
+ export default createTestServer;