@percy/core 1.8.1 → 1.10.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/dist/api.js CHANGED
@@ -12,6 +12,18 @@ export function createPercyServer(percy, port) {
12
12
  port
13
13
  }) // facilitate logger websocket connections
14
14
  .websocket('/(logger)?', ws => {
15
+ var _percy$testing, _percy$testing2;
16
+
17
+ // support sabotaging remote logging connections in testing mode
18
+ if (((_percy$testing = percy.testing) === null || _percy$testing === void 0 ? void 0 : _percy$testing.remoteLogging) === false) return ws.terminate(); // track all remote logging connections in testing mode
19
+
20
+ if (percy.testing) ((_percy$testing2 = percy.testing).remoteLoggers || (_percy$testing2.remoteLoggers = new Set())).add(ws);
21
+ ws.addEventListener('close', () => {
22
+ var _percy$testing3, _percy$testing3$remot;
23
+
24
+ return (_percy$testing3 = percy.testing) === null || _percy$testing3 === void 0 ? void 0 : (_percy$testing3$remot = _percy$testing3.remoteLoggers) === null || _percy$testing3$remot === void 0 ? void 0 : _percy$testing3$remot.delete(ws);
25
+ }); // listen for messages with specific logging payloads
26
+
15
27
  ws.addEventListener('message', ({
16
28
  data
17
29
  }) => {
@@ -23,13 +35,14 @@ export function createPercyServer(percy, port) {
23
35
  for (let m of messages) logger.instance.messages.add(m);
24
36
 
25
37
  if (log) logger.instance.log(...log);
26
- });
38
+ }); // respond with the current loglevel
39
+
27
40
  ws.send(JSON.stringify({
28
41
  loglevel: logger.loglevel()
29
42
  }));
30
43
  }) // general middleware
31
44
  .route((req, res, next) => {
32
- var _percy$testing, _percy$testing3, _percy$testing3$api, _percy$testing4, _percy$testing4$api;
45
+ var _percy$testing4, _percy$testing7, _percy$testing7$api, _percy$testing8, _percy$testing8$api;
33
46
 
34
47
  // treat all request bodies as json
35
48
  if (req.body) try {
@@ -38,35 +51,55 @@ export function createPercyServer(percy, port) {
38
51
 
39
52
  res.setHeader('Access-Control-Expose-Headers', '*, X-Percy-Core-Version'); // skip or change api version header in testing mode
40
53
 
41
- if (((_percy$testing = percy.testing) === null || _percy$testing === void 0 ? void 0 : _percy$testing.version) !== false) {
42
- var _percy$testing2;
54
+ if (((_percy$testing4 = percy.testing) === null || _percy$testing4 === void 0 ? void 0 : _percy$testing4.version) !== false) {
55
+ var _percy$testing5;
43
56
 
44
- res.setHeader('X-Percy-Core-Version', ((_percy$testing2 = percy.testing) === null || _percy$testing2 === void 0 ? void 0 : _percy$testing2.version) ?? pkg.version);
45
- } // support sabotaging requests in testing mode
57
+ res.setHeader('X-Percy-Core-Version', ((_percy$testing5 = percy.testing) === null || _percy$testing5 === void 0 ? void 0 : _percy$testing5.version) ?? pkg.version);
58
+ } // track all api reqeusts in testing mode
46
59
 
47
60
 
48
- if (((_percy$testing3 = percy.testing) === null || _percy$testing3 === void 0 ? void 0 : (_percy$testing3$api = _percy$testing3.api) === null || _percy$testing3$api === void 0 ? void 0 : _percy$testing3$api[req.url.pathname]) === 'error') {
49
- return res.json(500, {
50
- success: false,
51
- error: 'Error: testing'
61
+ if (percy.testing && !req.url.pathname.startsWith('/test/')) {
62
+ var _percy$testing6;
63
+
64
+ ((_percy$testing6 = percy.testing).requests || (_percy$testing6.requests = [])).push({
65
+ url: `${req.url.pathname}${req.url.search}`,
66
+ method: req.method,
67
+ body: req.body
52
68
  });
53
- } else if (((_percy$testing4 = percy.testing) === null || _percy$testing4 === void 0 ? void 0 : (_percy$testing4$api = _percy$testing4.api) === null || _percy$testing4$api === void 0 ? void 0 : _percy$testing4$api[req.url.pathname]) === 'disconnect') {
54
- return req.connection.destroy();
69
+ } // support sabotaging requests in testing mode
70
+
71
+
72
+ if (((_percy$testing7 = percy.testing) === null || _percy$testing7 === void 0 ? void 0 : (_percy$testing7$api = _percy$testing7.api) === null || _percy$testing7$api === void 0 ? void 0 : _percy$testing7$api[req.url.pathname]) === 'error') {
73
+ next = () => {
74
+ var _percy$testing$build;
75
+
76
+ return Promise.reject(new Error(((_percy$testing$build = percy.testing.build) === null || _percy$testing$build === void 0 ? void 0 : _percy$testing$build.error) || 'testing'));
77
+ };
78
+ } else if (((_percy$testing8 = percy.testing) === null || _percy$testing8 === void 0 ? void 0 : (_percy$testing8$api = _percy$testing8.api) === null || _percy$testing8$api === void 0 ? void 0 : _percy$testing8$api[req.url.pathname]) === 'disconnect') {
79
+ next = () => req.connection.destroy();
55
80
  } // return json errors
56
81
 
57
82
 
58
- return next().catch(e => res.json(e.status ?? 500, {
59
- build: percy.build,
60
- error: e.message,
61
- success: false
62
- }));
83
+ return next().catch(e => {
84
+ var _percy$testing9;
85
+
86
+ return res.json(e.status ?? 500, {
87
+ build: ((_percy$testing9 = percy.testing) === null || _percy$testing9 === void 0 ? void 0 : _percy$testing9.build) || percy.build,
88
+ error: e.message,
89
+ success: false
90
+ });
91
+ });
63
92
  }) // healthcheck returns basic information
64
- .route('get', '/percy/healthcheck', (req, res) => res.json(200, {
65
- loglevel: percy.loglevel(),
66
- config: percy.config,
67
- build: percy.build,
68
- success: true
69
- })) // get or set config options
93
+ .route('get', '/percy/healthcheck', (req, res) => {
94
+ var _percy$testing10;
95
+
96
+ return res.json(200, {
97
+ build: ((_percy$testing10 = percy.testing) === null || _percy$testing10 === void 0 ? void 0 : _percy$testing10.build) ?? percy.build,
98
+ loglevel: percy.loglevel(),
99
+ config: percy.config,
100
+ success: true
101
+ });
102
+ }) // get or set config options
70
103
  .route(['get', 'post'], '/percy/config', async (req, res) => res.json(200, {
71
104
  config: req.body ? await percy.setConfig(req.body) : percy.config,
72
105
  success: true
@@ -105,28 +138,46 @@ export function createPercyServer(percy, port) {
105
138
  }
106
139
  }, res) => {
107
140
  body = Buffer.isBuffer(body) ? body.toString() : body;
141
+ let {
142
+ remoteLoggers
143
+ } = percy.testing;
108
144
 
109
145
  if (cmd === 'reset') {
110
146
  // the reset command will reset testing mode and clear any logs
111
- percy.testing = {};
147
+ percy.testing = remoteLoggers ? {
148
+ remoteLoggers
149
+ } : {};
112
150
  logger.instance.messages.clear();
113
151
  } else if (cmd === 'version') {
114
152
  // the version command will update the api version header for testing
115
153
  percy.testing.version = body;
116
154
  } else if (cmd === 'error' || cmd === 'disconnect') {
155
+ var _percy$testing11;
156
+
117
157
  // the error or disconnect commands will cause specific endpoints to fail
118
- percy.testing.api = { ...percy.testing.api,
119
- [body]: cmd
158
+ ((_percy$testing11 = percy.testing).api || (_percy$testing11.api = {}))[body] = cmd;
159
+ } else if (cmd === 'build-failure') {
160
+ // the build-failure command will cause api errors to include a failed build
161
+ percy.testing.build = {
162
+ failed: true,
163
+ error: 'Build failed'
120
164
  };
165
+ } else if (cmd === 'remote-logging') {
166
+ // the remote-logging command will toggle remote logging support
167
+ if (body === false) remoteLoggers === null || remoteLoggers === void 0 ? void 0 : remoteLoggers.forEach(ws => ws.terminate());
168
+ percy.testing.remoteLogging = body;
121
169
  } else {
122
170
  // 404 for unknown commands
123
171
  return res.send(404);
124
172
  }
125
173
 
126
174
  return res.json(200, {
127
- testing: percy.testing
175
+ success: true
128
176
  });
129
- }) // returns an array of raw logs from the logger
177
+ }) // returns an array of raw requests made to the api
178
+ .route('get', '/test/requests', (req, res) => res.json(200, {
179
+ requests: percy.testing.requests
180
+ })) // returns an array of raw logs from the logger
130
181
  .route('get', '/test/logs', (req, res) => res.json(200, {
131
182
  logs: Array.from(logger.instance.messages)
132
183
  })) // serves a very basic html page for testing snapshots
@@ -138,15 +189,17 @@ export function createPercyServer(percy, port) {
138
189
  export function createStaticServer(options) {
139
190
  let {
140
191
  serve: dir,
141
- baseUrl = '/'
192
+ baseUrl = ''
142
193
  } = options;
143
- let server = Server.createServer(options); // used when generating an automatic sitemap
194
+ let server = Server.createServer(options); // remove trailing slashes so the base snapshot name matches other snapshots
195
+
196
+ baseUrl = baseUrl.replace(/\/$/, ''); // used when generating an automatic sitemap
144
197
 
145
198
  let toURL = Server.createRewriter( // reverse rewrites' src, dest, & order
146
- 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
199
+ 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
147
200
  !options.cleanUrls ? rewrite(filename) : rewrite(filename).replace(/(\/index)?\.html$/, '')), server.address())); // include automatic sitemap route
148
201
 
149
- server.route('get', '/sitemap.xml', async (req, res) => {
202
+ server.route('get', `${baseUrl}/sitemap.xml`, async (req, res) => {
150
203
  let {
151
204
  default: glob
152
205
  } = await import('fast-glob');
package/dist/percy.js CHANGED
@@ -31,7 +31,9 @@ export class Percy {
31
31
  deferUploads,
32
32
  // run without uploading anything
33
33
  skipUploads,
34
- // implies `skipUploads` and also skips asset discovery
34
+ // run without asset discovery
35
+ skipDiscovery,
36
+ // implies `skipUploads` and `skipDiscovery`
35
37
  dryRun,
36
38
  // implies `dryRun`, silent logs, and adds extra api endpoints
37
39
  testing,
@@ -53,6 +55,7 @@ export class Percy {
53
55
  this.testing = testing ? {} : null;
54
56
  this.dryRun = !!testing || !!dryRun;
55
57
  this.skipUploads = this.dryRun || !!skipUploads;
58
+ this.skipDiscovery = this.dryRun || !!skipDiscovery;
56
59
  this.delayUploads = this.skipUploads || !!delayUploads;
57
60
  this.deferUploads = this.delayUploads || !!deferUploads;
58
61
  if (this.deferUploads) this.#uploads.stop();
@@ -150,7 +153,7 @@ export class Percy {
150
153
  // at a later time when uploads are deferred, or run immediately when not deferred.
151
154
 
152
155
 
153
- async *start(options) {
156
+ async *start() {
154
157
  // already starting or started
155
158
  if (this.readyState != null) return;
156
159
  this.readyState = 0; // create a percy build as the first immediately queued task
@@ -193,10 +196,7 @@ export class Percy {
193
196
  // when not deferred, wait until the build is created first
194
197
  if (!this.deferUploads) await buildTask; // maybe launch the discovery browser
195
198
 
196
- if (!this.dryRun && (options === null || options === void 0 ? void 0 : options.browser) !== false) {
197
- yield this.browser.launch();
198
- } // start the server after everything else is ready
199
-
199
+ if (!this.skipDiscovery) yield this.browser.launch(); // start the server after everything else is ready
200
200
 
201
201
  yield (_this$server2 = this.server) === null || _this$server2 === void 0 ? void 0 : _this$server2.listen(); // mark instance as started
202
202
 
@@ -234,8 +234,8 @@ export class Percy {
234
234
  if (this.#snapshots.size) {
235
235
  if (close) this.#snapshots.close();
236
236
  yield* this.#snapshots.flush(s => {
237
- // do not log a count when not closing or while dry-running
238
- if (!close || this.dryRun) return;
237
+ // do not log a count when not closing or if asset discovery is disabled
238
+ if (!close || this.skipDiscovery) return;
239
239
  this.log.progress(`Processing ${s} snapshot${s !== 1 ? 's' : ''}...`, !!s);
240
240
  });
241
241
  } // run, close, and wait for the upload queue to empty
package/dist/snapshot.js CHANGED
@@ -117,6 +117,8 @@ export async function* gatherSnapshots(percy, options) {
117
117
  // other invalid options which are also scrubbed from the returned migrated options.
118
118
 
119
119
  export function validateSnapshotOptions(options) {
120
+ var _migrated$baseUrl;
121
+
120
122
  // decide which schema to validate against
121
123
  let schema = ['domSnapshot', 'dom-snapshot', 'dom_snapshot'].some(k => k in options) && '/snapshot/dom' || 'url' in options && '/snapshot' || 'sitemap' in options && '/snapshot/sitemap' || 'serve' in options && '/snapshot/server' || 'snapshots' in options && '/snapshot/list' || '/snapshot';
122
124
  let {
@@ -125,10 +127,12 @@ export function validateSnapshotOptions(options) {
125
127
  environmentInfo,
126
128
  snapshots,
127
129
  ...migrated
128
- } = PercyConfig.migrate(options, schema); // gather info for validating individual snapshot URLs
130
+ } = PercyConfig.migrate(options, schema); // maintain a trailing slash for base URLs to normalize them
131
+
132
+ if (((_migrated$baseUrl = migrated.baseUrl) === null || _migrated$baseUrl === void 0 ? void 0 : _migrated$baseUrl.endsWith('/')) === false) migrated.baseUrl += '/';
133
+ let baseUrl = schema === '/snapshot/server' ? 'http://localhost/' : migrated.baseUrl; // gather info for validating individual snapshot URLs
129
134
 
130
135
  let isSnapshot = schema === '/snapshot/dom' || schema === '/snapshot';
131
- let baseUrl = schema === '/snapshot/server' ? 'http://localhost' : options.baseUrl;
132
136
  let snaps = isSnapshot ? [migrated] : Array.isArray(snapshots) ? snapshots : [];
133
137
 
134
138
  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
@@ -370,7 +374,14 @@ export async function* discoverSnapshotResources(percy, snapshot, callback) {
370
374
 
371
375
  let cache = percy[RESOURCE_CACHE_KEY] || (percy[RESOURCE_CACHE_KEY] = new Map()); // preload the root resource for existing dom snapshots
372
376
 
373
- let resources = new Map(snapshot.domSnapshot && [createRootResource(snapshot.url, snapshot.domSnapshot)].map(resource => [resource.url, resource])); // open a new browser page
377
+ let resources = new Map(snapshot.domSnapshot && [createRootResource(snapshot.url, snapshot.domSnapshot)].map(resource => [resource.url, resource])); // when no discovery browser is available, do not attempt to discover other resources
378
+
379
+ if (percy.skipDiscovery && !snapshot.domSnapshot) {
380
+ throw new Error('Cannot capture DOM snapshot when asset discovery is disabled');
381
+ } else if (percy.skipDiscovery) {
382
+ return handleSnapshotResources(snapshot, resources, callback);
383
+ } // open a new browser page
384
+
374
385
 
375
386
  let page = yield percy.browser.page({
376
387
  enableJavaScript: snapshot.enableJavaScript ?? !snapshot.domSnapshot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/core",
3
- "version": "1.8.1",
3
+ "version": "1.10.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,10 +39,10 @@
39
39
  "test:types": "tsd"
40
40
  },
41
41
  "dependencies": {
42
- "@percy/client": "1.8.1",
43
- "@percy/config": "1.8.1",
44
- "@percy/dom": "1.8.1",
45
- "@percy/logger": "1.8.1",
42
+ "@percy/client": "1.10.0",
43
+ "@percy/config": "1.10.0",
44
+ "@percy/dom": "1.10.0",
45
+ "@percy/logger": "1.10.0",
46
46
  "content-disposition": "^0.5.4",
47
47
  "cross-spawn": "^7.0.3",
48
48
  "extract-zip": "^2.0.1",
@@ -53,5 +53,5 @@
53
53
  "rimraf": "^3.0.2",
54
54
  "ws": "^8.0.0"
55
55
  },
56
- "gitHead": "497772c62e2fa2d763b350c40a0cfec918f7125f"
56
+ "gitHead": "a6934eda4fc3b84845ae606d7f5a901f25e0a56f"
57
57
  }