-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ✨ Add ability to snapshot a directory of static images (#353)
* feat: ✨ Add ability to snapshot a directory of static images * ✅ Add integration test for upload command * feat: ✨ Use image dimensions for snapshot dimensions * ✅ Add CI steps for snapshot and upload commands * ✅ Update snapshot command's test script * fix: 🐛 Use absolute image pathnames and exit with non-zero on error * feat: ✨🔊 Better logging when uploading static images * ✅ Appropriately size static test images * fix: 🐛 Use correct min-height option * ✅🔧 Skip static-snapshot test It currently uploads 0 snapshots and needs to be investigated * feat: ✨ Error and exit when no matching files can be found * 📝 Add upload command to readme * feat: ✨ Add ignore option to image upload command
- Loading branch information
Wil Wilsman
authored
Sep 25, 2019
1 parent
a6034d2
commit 96f1ed5
Showing
11 changed files
with
280 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { Command, flags } from '@oclif/command' | ||
import { DEFAULT_CONFIGURATION } from '../configuration/configuration' | ||
import ConfigurationService from '../services/configuration-service' | ||
import ImageSnapshotService from '../services/image-snapshot-service' | ||
|
||
export default class Upload extends Command { | ||
static description = 'Upload a directory containing static snapshot images.' | ||
static hidden = false | ||
|
||
static args = [{ | ||
name: 'uploadDirectory', | ||
description: 'A path to the directory containing static snapshot images', | ||
required: true, | ||
}] | ||
|
||
static examples = [ | ||
'$ percy upload _images/', | ||
'$ percy upload _images/ --files **/*.png', | ||
] | ||
|
||
static flags = { | ||
files: flags.string({ | ||
char: 'f', | ||
description: 'Glob or comma-seperated string of globs for matching the files and directories to snapshot.', | ||
default: DEFAULT_CONFIGURATION['image-snapshots'].files, | ||
}), | ||
ignore: flags.string({ | ||
char: 'i', | ||
description: 'Glob or comma-seperated string of globs for matching the files and directories to ignore.', | ||
default: DEFAULT_CONFIGURATION['image-snapshots'].ignore, | ||
}), | ||
} | ||
|
||
percyToken: string = process.env.PERCY_TOKEN || '' | ||
|
||
percyTokenPresent(): boolean { | ||
return this.percyToken.trim() !== '' | ||
} | ||
|
||
async run() { | ||
// exit gracefully if percy token was not provided | ||
if (!this.percyTokenPresent()) { | ||
this.warn('PERCY_TOKEN was not provided.') | ||
this.exit(0) | ||
} | ||
|
||
const { args, flags } = this.parse(Upload) | ||
|
||
const configurationService = new ConfigurationService() | ||
configurationService.applyFlags(flags) | ||
configurationService.applyArgs(args) | ||
const configuration = configurationService.configuration | ||
|
||
// upload snapshot images | ||
const imageSnapshotService = new ImageSnapshotService(configuration['image-snapshots']) | ||
await imageSnapshotService.snapshotAll() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export interface ImageSnapshotsConfiguration { | ||
path: string, | ||
files: string, | ||
ignore: string, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import * as crypto from 'crypto' | ||
import * as fs from 'fs' | ||
import * as globby from 'globby' | ||
import { imageSize } from 'image-size' | ||
import * as os from 'os' | ||
import * as path from 'path' | ||
|
||
import { DEFAULT_CONFIGURATION } from '../configuration/configuration' | ||
import { ImageSnapshotsConfiguration } from '../configuration/image-snapshots-configuration' | ||
import logger, { logError, profile } from '../utils/logger' | ||
import BuildService from './build-service' | ||
import PercyClientService from './percy-client-service' | ||
|
||
const ALLOWED_IMAGE_TYPES = /\.(png|jpg|jpeg)$/i | ||
|
||
export default class ImageSnapshotService extends PercyClientService { | ||
private readonly buildService: BuildService | ||
private readonly configuration: ImageSnapshotsConfiguration | ||
|
||
constructor(configuration?: ImageSnapshotsConfiguration) { | ||
super() | ||
|
||
this.buildService = new BuildService() | ||
this.configuration = configuration || DEFAULT_CONFIGURATION['image-snapshots'] | ||
} | ||
|
||
get buildId() { | ||
return this.buildService.buildId | ||
} | ||
|
||
makeLocalCopy(imagePath: string) { | ||
logger.debug(`Making local copy of image: ${imagePath}`) | ||
|
||
const buffer = fs.readFileSync(path.resolve(this.configuration.path, imagePath)) | ||
const sha = crypto.createHash('sha256').update(buffer).digest('hex') | ||
const filename = path.join(os.tmpdir(), sha) | ||
|
||
if (!fs.existsSync(filename)) { | ||
fs.writeFileSync(filename, buffer) | ||
} else { | ||
logger.debug(`Skipping file copy [already_copied]: ${imagePath}`) | ||
} | ||
|
||
return filename | ||
} | ||
|
||
buildResources(imagePath: string): any[] { | ||
const { name, ext } = path.parse(imagePath) | ||
const localCopy = this.makeLocalCopy(imagePath) | ||
const mimetype = ext === '.png' ? 'image/png' : 'image/jpeg' | ||
const sha = path.basename(localCopy) | ||
|
||
const rootResource = this.percyClient.makeResource({ | ||
isRoot: true, | ||
resourceUrl: `/${name}`, | ||
mimetype: 'text/html', | ||
content: ` | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>${imagePath}</title> | ||
<style> | ||
html, body, img { width: 100%; margin: 0; padding: 0; font-size: 0; } | ||
</style> | ||
</head> | ||
<body> | ||
<img src="/${imagePath}"/> | ||
</body> | ||
</html> | ||
`, | ||
}) | ||
|
||
const imgResource = this.percyClient.makeResource({ | ||
resourceUrl: `/${imagePath}`, | ||
localPath: localCopy, | ||
mimetype, | ||
sha, | ||
}) | ||
|
||
return [rootResource, imgResource] | ||
} | ||
|
||
async createSnapshot( | ||
name: string, | ||
resources: any[], | ||
width: number, | ||
height: number, | ||
): Promise<any> { | ||
return this.percyClient.createSnapshot(this.buildId, resources, { | ||
name, | ||
widths: [width], | ||
minimumHeight: height, | ||
}).then(async (response: any) => { | ||
await this.percyClient.uploadMissingResources(this.buildId, response, resources) | ||
return response | ||
}).then(async (response: any) => { | ||
const snapshotId = response.body.data.id | ||
profile('-> imageSnapshotService.finalizeSnapshot') | ||
await this.percyClient.finalizeSnapshot(snapshotId) | ||
profile('-> imageSnapshotService.finalizeSnapshot', { snapshotId }) | ||
return response | ||
}).catch(logError) | ||
} | ||
|
||
async snapshotAll() { | ||
try { | ||
// intentially remove '' values from because that matches every file | ||
const globs = this.configuration.files.split(',').filter(Boolean) | ||
const ignore = this.configuration.ignore.split(',').filter(Boolean) | ||
const paths = await globby(globs, { cwd: this.configuration.path, ignore }) | ||
|
||
if (!paths.length) { | ||
logger.error(`no matching files found in '${this.configuration.path}''`) | ||
logger.info('exiting') | ||
return process.exit(1) | ||
} | ||
|
||
await this.buildService.create() | ||
logger.debug('uploading snapshots of static images') | ||
|
||
// wait for snapshots in parallel | ||
await Promise.all(paths.reduce((promises, pathname) => { | ||
logger.debug(`handling snapshot: '${pathname}'`) | ||
|
||
// only snapshot supported images | ||
if (!pathname.match(ALLOWED_IMAGE_TYPES)) { | ||
logger.info(`skipping unsupported image type: '${pathname}'`) | ||
return promises | ||
} | ||
|
||
// @ts-ignore - if dimensions are undefined, the library throws an error | ||
const { width, height } = imageSize(path.resolve(this.configuration.path, pathname)) | ||
|
||
const resources = this.buildResources(pathname) | ||
const snapshotPromise = this.createSnapshot(pathname, resources, width, height) | ||
logger.info(`snapshot uploaded: '${pathname}'`) | ||
promises.push(snapshotPromise) | ||
|
||
return promises | ||
}, [] as any[])) | ||
|
||
// finalize build | ||
await this.buildService.finalize() | ||
} catch (error) { | ||
logError(error) | ||
process.exit(1) | ||
} | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters