-
Notifications
You must be signed in to change notification settings - Fork 40
/
snapshot.js
137 lines (122 loc) · 4.3 KB
/
snapshot.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import fs from 'fs';
import path from 'path';
import { pathToFileURL } from 'url';
import command from '@percy/cli-command';
import * as SnapshotConfig from './config.js';
export const snapshot = command('snapshot', {
description: 'Snapshot a static directory, snapshots file, or sitemap URL',
args: [{
name: 'dir|file|sitemap',
description: 'Static directory, snapshots file, or sitemap url',
required: true,
attribute: val => {
if (/^https?:\/\//.test(val)) return 'sitemap';
if (!fs.existsSync(val)) throw new Error(`Not found: ${val}`);
return fs.lstatSync(val).isDirectory() ? 'serve' : 'file';
}
}],
flags: [{
name: 'base-url',
description: 'The base url pages are hosted at when snapshotting',
type: 'string',
short: 'b'
}, {
name: 'include',
description: 'One or more globs/patterns matching snapshots to include',
type: 'pattern',
multiple: true
}, {
name: 'exclude',
description: 'One or more globs/patterns matching snapshots to exclude',
type: 'pattern',
multiple: true
}, {
// static only
name: 'clean-urls',
description: 'Rewrite static index and filepath URLs to be clean',
percyrc: 'static.cleanUrls',
group: 'Static'
}],
examples: [
'$0 ./public',
'$0 snapshots.yml',
'$0 https://percy.io/sitemap.xml'
],
percy: {
delayUploads: true,
projectType: 'web'
},
config: {
schemas: [SnapshotConfig.configSchema],
migrations: [SnapshotConfig.configMigration]
}
}, async function*({ percy, args, flags, log, exit }) {
let { include, exclude, baseUrl, cleanUrls } = flags;
let { file, serve, sitemap } = args;
// parse and validate the --base-url flag after args are parsed
if (file || serve) baseUrl &&= parseBaseUrl(baseUrl, !!serve);
// only continue if percy is not disabled
if (!percy) exit(0, 'Percy is disabled');
try {
let options;
/* istanbul ignore else: arg is required and always one of these */
if (file) {
// load snapshots file
let snapshots = yield loadSnapshotFile(file);
// remove any references and accept an array of snapshots instead of an config object
let { references, ...config } = Array.isArray(snapshots) ? { snapshots } : snapshots;
options = merge(config, { baseUrl, include, exclude });
} else if (serve) {
// serve and snapshot a static directory
let config = { serve, cleanUrls, baseUrl, include, exclude };
options = merge(percy.config.static, config);
} else if (sitemap) {
// fetch urls to snapshot from a sitemap
let config = { sitemap, include, exclude };
options = merge(percy.config.sitemap, config);
}
yield* percy.yield.start();
yield* percy.yield.snapshot(options);
yield* percy.yield.stop();
} catch (error) {
await percy.stop(true);
throw error;
}
});
// Validates the provided `--base-url` flag and returns a `baseUrl` string if valid. The flag is
// validated and parsed differently for static directories and snapshot files.
function parseBaseUrl(baseUrl, pathOnly) {
try {
let needsHost = pathOnly && baseUrl.startsWith('/');
let url = new URL(baseUrl, needsHost ? 'http://localhost' : undefined);
return pathOnly ? url.pathname : url.href;
} catch (e) {
throw new Error("The '--base-url' flag must " + (pathOnly
? 'start with a forward slash (/) when providing a static directory'
: 'include a protocol and hostname when providing a list of snapshots'
));
}
}
// Small shallow merge util that does not merge null or undefined values.
function merge(...objs) {
return objs.reduce((target, obj) => {
for (let k in obj) target[k] = obj[k] ?? target[k];
return target;
}, {});
}
// Loads snapshot options from a js, json, or yaml file.
async function loadSnapshotFile(file) {
let ext = path.extname(file);
if (/\.(c|m)?js$/.test(ext)) {
let { default: module } = await import(pathToFileURL(file));
return typeof module === 'function' ? await module() : module;
} else if (ext === '.json') {
return JSON.parse(fs.readFileSync(file, 'utf-8'));
} else if (/\.ya?ml$/.test(ext)) {
let { default: YAML } = await import('yaml');
return YAML.parse(fs.readFileSync(file, 'utf-8'));
} else {
throw new Error(`Unsupported filetype: ${file}`);
}
}
export default snapshot;