Skip to content

Commit

Permalink
✨ Allow snapshotting a list of pages to utilize the base-url flag (#382)
Browse files Browse the repository at this point in the history
* ✨ Allow base command initial overrides argument with percyrc method

* ✨ Allow base-url to work with a list of page pathnames

Also allow a list of page URLs rather than requiring objects containing URLs

* ✨ Allow custom schema validation error messages

* ✨ Add pattern validation to static.base-url config option

* 🐛 Fix snapshot name default uri path

* 🚨 Fix lint issues
  • Loading branch information
Wil Wilsman authored Jun 21, 2021
1 parent 9f12101 commit f7f73d1
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 95 deletions.
4 changes: 2 additions & 2 deletions packages/cli-command/src/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ export default class PercyCommand extends Command {
// respective `percyrc` parameter. The flag input is then merged with options
// loaded from a config file and default config options. The PERCY_TOKEN
// environment variable is also included as a convenience.
percyrc() {
percyrc(initialOverrides = {}) {
let flags = Object.entries(this.constructor.flags);
let overrides = flags.reduce((conf, [name, flag]) => (
flag.percyrc?.split('.').reduce((target, key, i, paths) => {
let last = i === paths.length - 1;
target[key] = last ? this.flags[name] : (target[key] ?? {});
return last ? conf : target[key];
}, conf) ?? conf
), {});
), initialOverrides);

// will also validate config and log warnings
let config = PercyConfig.load({
Expand Down
140 changes: 84 additions & 56 deletions packages/cli-snapshot/src/commands/snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import YAML from 'yaml';
import { schema } from '../config';
import pkg from '../../package.json';

// Throw a better error message for invalid urls
function validURL(input, base) {
try { return new URL(input, base || undefined); } catch (error) {
throw new Error(`Invalid URL: ${error.input}`);
}
}

export class Snapshot extends Command {
static description = 'Snapshot a list of pages from a file or directory';

Expand All @@ -23,27 +30,26 @@ export class Snapshot extends Command {
...flags.config,

'base-url': flags.string({
char: 'b',
description: 'the url path to serve the static directory from',
default: schema.static.properties.baseUrl.default,
percyrc: 'static.baseUrl'
description: 'the base url pages are hosted at when snapshotting',
char: 'b'
}),
'dry-run': flags.boolean({
description: 'prints a list of pages to snapshot without snapshotting',
char: 'd'
}),

// static only flags
files: flags.string({
char: 'f',
multiple: true,
description: 'one or more globs matching static file paths to snapshot',
default: schema.static.properties.files.default,
percyrc: 'static.files'
percyrc: 'static.files',
multiple: true
}),
ignore: flags.string({
char: 'i',
multiple: true,
description: 'one or more globs matching static file paths to ignore',
percyrc: 'static.ignore'
}),
'dry-run': flags.boolean({
char: 'd',
description: 'prints a list of pages to snapshot without snapshotting'
default: schema.static.properties.ignore.default,
percyrc: 'static.ignore',
multiple: true
})
};

Expand All @@ -56,52 +62,72 @@ export class Snapshot extends Command {

async run() {
if (!this.isPercyEnabled()) {
this.log.info('Percy is disabled. Skipping snapshots');
return;
return this.log.info('Percy is disabled. Skipping snapshots');
}

let config = this.percyrc();
let { pathname } = this.args;

if (!fs.existsSync(pathname)) {
return this.error(`Not found: ${pathname}`);
} else if (config.static.baseUrl[0] !== '/') {
return this.error('The base-url flag must begin with a forward slash (/)');
this.error(`Not found: ${pathname}`);
}

let { 'base-url': baseUrl, 'dry-run': dry } = this.flags;
let isStatic = fs.lstatSync(pathname).isDirectory();

if (baseUrl) {
if (isStatic && !baseUrl.startsWith('/')) {
this.error('The base-url must begin with a forward slash (/) ' + (
'when snapshotting static directories'));
} else if (!isStatic && !baseUrl.startsWith('http')) {
this.error('The base-url must include a protocol and hostname ' + (
'when snapshotting a list of pages'));
}
}

let pages = fs.lstatSync(pathname).isDirectory()
? await this.loadStaticPages(pathname, config.static)
this.percy = new Percy({
...this.percyrc({ static: isStatic ? { baseUrl } : null }),
clientInfo: `${pkg.name}/${pkg.version}`,
server: false
});

let pages = isStatic
? await this.loadStaticPages(pathname)
: await this.loadPagesFile(pathname);

if (!pages.length) {
return this.error('No snapshots found');
}
pages = pages.map(page => {
// allow a list of urls
if (typeof page === 'string') page = { url: page };

if (this.flags['dry-run']) {
let list = pages.reduce((acc, { name, additionalSnapshots = [] }) => {
return acc.concat(name, additionalSnapshots.map(add => {
let { prefix = '', suffix = '' } = add;
return add.name || `${prefix}${name}${suffix}`;
}));
}, []);

return this.log.info(`Found ${list.length} ` + (
`snapshot${list.length === 1 ? '' : 's'}:\n` +
list.join('\n')
));
}
// validate and prepend the baseUrl
let uri = validURL(page.url, !isStatic && baseUrl);
page.url = uri.href;

this.percy = await Percy.start({
clientInfo: `${pkg.name}/${pkg.version}`,
server: false,
...config
// default page name to url /pathname?search#hash
page.name ||= `${uri.pathname}${uri.search}${uri.hash}`;

return page;
});

let l = pages.length;
if (!l) this.error('No snapshots found');

if (!dry) await this.percy.start();
else this.log.info(`Found ${l} snapshot${l === 1 ? '' : 's'}`);

for (let page of pages) {
this.percy.snapshot(page);
if (dry) {
this.log.info(`Snapshot found: ${page.name}`);
this.log.debug(`-> url: ${page.url}`);

for (let s of (page.additionalSnapshots || [])) {
let name = s.name || `${s.prefix || ''}${page.name}${s.suffix || ''}`;
this.log.info(`Snapshot found: ${name}`);
this.log.debug(`-> url: ${page.url}`);
}
} else {
this.percy.snapshot(page);
}
}

await this.percy.idle();
}

// Called on error, interupt, or after running
Expand Down Expand Up @@ -131,19 +157,21 @@ export class Snapshot extends Command {
}

// Starts a static server and returns a list of pages to snapshot.
async loadStaticPages(pathname, { baseUrl, files, ignore }) {
ignore = [].concat(ignore).filter(Boolean);
let paths = await globby(files, { cwd: pathname, ignore });
let addr = '';
async loadStaticPages(pathname) {
let { baseUrl, files, ignore } = this.percy.config.static;

if (!this.flags['dry-run']) {
addr = await this.serve(pathname, baseUrl);
}
let paths = await globby(files, {
ignore: [].concat(ignore).filter(Boolean),
cwd: pathname
});

let addr = !this.flags['dry-run']
? await this.serve(pathname, baseUrl)
: 'http://localhost';

return paths.sort().map(path => ({
url: `${addr}${baseUrl}${path}`,
name: `${baseUrl}${path}`
}));
return paths.sort().map(path => (
`${addr}${baseUrl}${path}`
));
}

// Loads pages to snapshot from a js, json, or yaml file.
Expand Down
6 changes: 5 additions & 1 deletion packages/cli-snapshot/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ export const schema = {
properties: {
baseUrl: {
type: 'string',
default: '/'
pattern: '^/',
default: '/',
errors: {
pattern: 'must start with a forward slash (/)'
}
},
files: {
anyOf: [
Expand Down
Loading

0 comments on commit f7f73d1

Please sign in to comment.