Skip to content

Commit 1175e55

Browse files
committed
✨ Add @percy/cli-upload package
1 parent e2e2f69 commit 1175e55

10 files changed

Lines changed: 323 additions & 1 deletion

File tree

packages/cli-upload/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# @percy/cli-upload
2+
3+
Percy CLI command to uploade a directory of static images to Percy for diffing.
4+
5+
## Commands
6+
<!-- commands -->
7+
* [`percy upload DIRNAME`](#percy-upload-dirname)
8+
9+
## `percy upload DIRNAME`
10+
11+
Upload a directory of images to Percy
12+
13+
```
14+
USAGE
15+
$ percy upload DIRNAME
16+
17+
ARGUMENTS
18+
DIRNAME directory of images to upload
19+
20+
OPTIONS
21+
-c, --config=config configuration file path
22+
-d, --dry-run prints a list of matching images to upload without uploading
23+
-f, --files=files [default: **/*.{png,jpg,jpeg}] one or more globs matching image file paths to upload
24+
-i, --ignore=ignore one or more globs matching image file paths to ignore
25+
-q, --quiet log errors only
26+
-v, --verbose log everything
27+
--silent log nothing
28+
29+
EXAMPLE
30+
$ percy upload ./images
31+
```
32+
<!-- commandsstop -->
33+
34+
## Percy Configuration
35+
36+
This CLI plugin adds the following Percy configuration options (defaults shown).
37+
38+
```yaml
39+
# defaults
40+
version: 2
41+
upload:
42+
files: '**/*.{png,jpg,jpeg}'
43+
ignore: ''
44+
```

packages/cli-upload/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@percy/cli-upload",
3+
"version": "1.0.0",
4+
"license": "MIT",
5+
"main": "dist/index.js",
6+
"files": [
7+
"./dist",
8+
"./oclif.manifest.json"
9+
],
10+
"scripts": {
11+
"build": "babel --root-mode upward src --out-dir dist",
12+
"lint": "eslint --ignore-path ../../.gitignore .",
13+
"postpublish": "rm -f oclif.manifest.json",
14+
"prepublish": "oclif-dev manifest",
15+
"readme": "oclif-dev readme",
16+
"test": "cross-env NODE_ENV=test mocha",
17+
"test:coverage": "nyc yarn test"
18+
},
19+
"mocha": {
20+
"require": "../../scripts/babel-register"
21+
},
22+
"oclif": {
23+
"bin": "percy",
24+
"commands": "./dist/commands",
25+
"hooks": {
26+
"init": "./dist/hooks/init"
27+
}
28+
},
29+
"dependencies": {
30+
"@percy/cli-command": "1.0.0",
31+
"@percy/client": "1.0.0",
32+
"@percy/logger": "1.0.0",
33+
"globby": "^11.0.0",
34+
"image-size": "^0.8.3"
35+
}
36+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import Command, { flags } from '@percy/cli-command';
4+
import log from '@percy/logger';
5+
import globby from 'globby';
6+
import imageSize from 'image-size';
7+
import PercyClient from '@percy/client';
8+
import createImageResources from '../resources';
9+
import { schema } from '../config';
10+
11+
const ALLOWED_IMAGE_TYPES = /\.(png|jpg|jpeg)$/i;
12+
13+
export class Upload extends Command {
14+
static description = 'Upload a directory of images to Percy';
15+
16+
static args = [{
17+
name: 'dirname',
18+
description: 'directory of images to upload',
19+
required: true
20+
}];
21+
22+
static flags = {
23+
...flags.logging,
24+
...flags.config,
25+
26+
files: flags.glob({
27+
char: 'f',
28+
multiple: true,
29+
description: 'one or more globs matching image file paths to upload',
30+
default: schema.upload.properties.files.default,
31+
percyrc: 'upload.files'
32+
}),
33+
ignore: flags.glob({
34+
char: 'i',
35+
multiple: true,
36+
description: 'one or more globs matching image file paths to ignore',
37+
percyrc: 'upload.ignore'
38+
}),
39+
'dry-run': flags.boolean({
40+
char: 'd',
41+
description: 'prints a list of matching images to upload without uploading'
42+
})
43+
};
44+
45+
static examples = [
46+
'$ percy upload ./images'
47+
];
48+
49+
async run() {
50+
if (!this.isPercyEnabled()) {
51+
log.info('Percy is disabled. Skipping upload');
52+
return;
53+
}
54+
55+
let { dirname } = this.args;
56+
57+
if (!fs.existsSync(dirname)) {
58+
return this.error(`Not found: ${dirname}`);
59+
} else if (!fs.lstatSync(dirname).isDirectory()) {
60+
return this.error(`Not a directory: ${dirname}`);
61+
}
62+
63+
let { upload: { files, ignore } } = this.percyrc();
64+
ignore = [].concat(ignore).filter(Boolean);
65+
66+
let paths = await globby(files, { cwd: dirname, ignore });
67+
paths.sort();
68+
69+
if (!paths.length) {
70+
return this.error(`No matching files found in '${dirname}'`);
71+
} else if (this.flags['dry-run']) {
72+
log.info('Matching files:');
73+
return paths.forEach(p => console.log(p));
74+
}
75+
76+
// we already have assets so we don't need asset discovery from @percy/core,
77+
// we can use @percy/client directly to send snapshots
78+
this.client = new PercyClient();
79+
await this.client.createBuild();
80+
log.info('Percy has started!');
81+
82+
let build = this.client.build;
83+
log.info(`Created build #${build.number}: ${build.url}`);
84+
85+
for (let name of paths) {
86+
log.debug(`Uploading snapshot: ${name}`);
87+
88+
// only snapshot supported images
89+
if (!name.match(ALLOWED_IMAGE_TYPES)) {
90+
log.info(`Skipping unsupported image type: ${name}`);
91+
continue;
92+
}
93+
94+
let filepath = path.resolve(dirname, name);
95+
let buffer = fs.readFileSync(filepath);
96+
let { width, height } = imageSize(filepath);
97+
98+
await this.client.sendSnapshot({
99+
// width and height is clamped to API min and max
100+
widths: [Math.max(10, Math.min(width, 2000))],
101+
minimumHeight: Math.max(10, Math.min(height, 2000)),
102+
resources: createImageResources(name, buffer, width, height),
103+
name
104+
});
105+
106+
log.info(`Snapshot uploaded: ${name}`);
107+
}
108+
}
109+
110+
// Finalize the build when finished
111+
async finally() {
112+
let build = this.client?.build;
113+
114+
if (build?.id) {
115+
await this.client?.finalizeBuild();
116+
log.info(`Finalized build #${build.number}: ${build.url}`);
117+
}
118+
}
119+
}

packages/cli-upload/src/config.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export const schema = {
2+
upload: {
3+
type: 'object',
4+
additionalProperties: false,
5+
properties: {
6+
files: {
7+
anyOf: [
8+
{ type: 'string' },
9+
{ type: 'array', items: { type: 'string' } }
10+
],
11+
default: '**/*.{png,jpg,jpeg}'
12+
},
13+
ignore: {
14+
anyOf: [
15+
{ type: 'string' },
16+
{ type: 'array', items: { type: 'string' } }
17+
],
18+
default: ''
19+
}
20+
}
21+
}
22+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import PercyConfig from '@percy/cli-config';
2+
import { schema } from '../config';
3+
4+
export default function() {
5+
PercyConfig.addSchema(schema);
6+
}

packages/cli-upload/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import path from 'path';
2+
import { sha256hash } from '@percy/client/dist/utils';
3+
4+
// Returns a root resource object with a sha and mimetype.
5+
function createRootResource(url, content) {
6+
return {
7+
url,
8+
content,
9+
sha: sha256hash(content),
10+
mimetype: 'text/html',
11+
root: true
12+
};
13+
}
14+
15+
// Returns an image resource object with a sha.
16+
function createImageResource(url, content, mimetype) {
17+
return {
18+
url,
19+
content,
20+
sha: sha256hash(content),
21+
mimetype
22+
};
23+
}
24+
25+
// Returns root resource and image resource objects based on an image's
26+
// filename, contents, and dimensions. The root resource is a generated DOM
27+
// designed to display an image at it's native size without margins or padding.
28+
export default function createImageResources(filename, content, width, height) {
29+
let { name, ext } = path.parse(filename);
30+
let url = `/${encodeURIComponent(filename)}`;
31+
let mimetype = ext === '.png' ? 'image/png' : 'image/jpeg';
32+
33+
return [
34+
createRootResource(encodeURIComponent(name), `
35+
<!doctype html>
36+
<html lang="en">
37+
<head>
38+
<meta charset="utf-8">
39+
<title>${filename}</title>
40+
<style>
41+
*, *::before, *::after { margin: 0; padding: 0; font-size: 0; }
42+
html, body { width: 100%; }
43+
img { max-width: 100%; }
44+
</style>
45+
</head>
46+
<body>
47+
<img src="${url}" width="${width}px" height="${height}px"/>
48+
</body>
49+
</html>
50+
`),
51+
createImageResource(url, content, mimetype)
52+
];
53+
}

packages/cli/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ A collection of CLI commmands for taking Percy snapshots.
1313
* [`percy finalize`](#percy-finalize)
1414
* [`percy help [COMMAND]`](#percy-help-command)
1515
* [`percy snapshot PATHNAME`](#percy-snapshot-pathname)
16+
* [`percy upload DIRNAME`](#percy-upload-dirname)
1617

1718
## `percy config:create [FILEPATH]`
1819

@@ -197,4 +198,28 @@ EXAMPLES
197198
$ percy snapshot ./public
198199
$ percy snapshot pages.yml
199200
```
201+
202+
## `percy upload DIRNAME`
203+
204+
upload a directory of images
205+
206+
```
207+
USAGE
208+
$ percy upload DIRNAME
209+
210+
ARGUMENTS
211+
DIRNAME directory of images to upload
212+
213+
OPTIONS
214+
-c, --config=config configuration file path
215+
-d, --dry-run prints a list of matching images to upload without uploading
216+
-f, --files=files [default: **/*.{png,jpg,jpeg}] one or more globs matching image file paths to upload
217+
-i, --ignore=ignore one or more globs matching image file paths to ignore
218+
-q, --quiet log errors only
219+
-v, --verbose log everything
220+
--silent log nothing
221+
222+
EXAMPLE
223+
$ percy upload ./images
224+
```
200225
<!-- commandsstop -->

packages/cli/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@percy/cli-exec",
2626
"@percy/cli-finalize",
2727
"@percy/cli-snapshot",
28+
"@percy/cli-upload",
2829
"@oclif/plugin-help"
2930
]
3031
},
@@ -33,6 +34,7 @@
3334
"@percy/cli-config": "1.0.0",
3435
"@percy/cli-exec": "1.0.0",
3536
"@percy/cli-finalize": "1.0.0",
36-
"@percy/cli-snapshot": "1.0.0"
37+
"@percy/cli-snapshot": "1.0.0",
38+
"@percy/cli-upload": "1.0.0"
3739
}
3840
}

yarn.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5679,6 +5679,13 @@ ignore@^5.1.1, ignore@^5.1.4:
56795679
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
56805680
integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
56815681

5682+
image-size@^0.8.3:
5683+
version "0.8.3"
5684+
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.8.3.tgz#f0b568857e034f29baffd37013587f2c0cad8b46"
5685+
integrity sha512-SMtq1AJ+aqHB45c3FsB4ERK0UCiA2d3H1uq8s+8T0Pf8A3W4teyBQyaFaktH6xvZqh+npwlKU7i4fJo0r7TYTg==
5686+
dependencies:
5687+
queue "6.0.1"
5688+
56825689
import-fresh@^2.0.0:
56835690
version "2.0.0"
56845691
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
@@ -8443,6 +8450,13 @@ querystring@0.2.0:
84438450
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
84448451
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
84458452

8453+
queue@6.0.1:
8454+
version "6.0.1"
8455+
resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.1.tgz#abd5a5b0376912f070a25729e0b6a7d565683791"
8456+
integrity sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg==
8457+
dependencies:
8458+
inherits "~2.0.3"
8459+
84468460
quick-lru@^1.0.0:
84478461
version "1.1.0"
84488462
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"

0 commit comments

Comments
 (0)