Skip to content

Commit

Permalink
Fix duplicate asset clobbering resource paths. (#70)
Browse files Browse the repository at this point in the history
* Add test for asset clobbering.

* Fix duplicate asset clobbering resource paths.

* Fix lint errors.
  • Loading branch information
fotinakis committed Jul 20, 2018
1 parent 2782e23 commit ac32ea6
Show file tree
Hide file tree
Showing 7 changed files with 931 additions and 1,032 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -16,6 +16,7 @@
/libpeerconnection.log
npm-debug.log*
testem.log
.DS_Store

# ember-try
.node_modules.ember-try/
Expand Down
79 changes: 12 additions & 67 deletions index.js
Expand Up @@ -2,15 +2,11 @@

'use strict';

var crypto = require('crypto');
var fs = require('fs');
var path = require('path');

var bodyParser = require('body-parser');
var PercyClient = require('percy-client');
var Environment = require('percy-client/dist/environment');
var PromisePool = require('es6-promise-pool');
var walk = require('walk');

// Some build assets we never want to upload.
var SKIPPED_ASSETS = [
Expand All @@ -28,62 +24,6 @@ var SKIPPED_ASSETS = [
/\.log$/,
/\.DS_Store$/
];
var MAX_FILE_SIZE_BYTES = 15728640; // 15MB.

// Synchronously walk the build directory, read each file and calculate its SHA 256 hash,
// and create a mapping of hashes to Resource objects.
function gatherBuildResources(percyClient, buildDir) {
var hashToResource = {};
var walkOptions = {
// Follow symlinks because many assets in the ember build directory are just symlinks.
followLinks: true,

listeners: {
file: function (root, fileStats, next) {
var absolutePath = path.join(root, fileStats.name);
var resourceUrl = absolutePath.replace(buildDir, '');

if (path.sep == '\\') {
// Windows support: transform filesystem backslashes into forward-slashes for the URL.
resourceUrl = resourceUrl.replace(/\\/g, '/');
}

// Append the Ember rootURL if it exists.
resourceUrl = normalizedRootUrl + resourceUrl;

for (var i in SKIPPED_ASSETS) {
if (resourceUrl.match(SKIPPED_ASSETS[i])) {
next();
return;
}
}

// Skip large files.
if (fs.statSync(absolutePath)['size'] > MAX_FILE_SIZE_BYTES) {
console.warn('\n[percy][WARNING] Skipping large build resource: ', resourceUrl);
return;
}

// TODO(fotinakis): this is synchronous and potentially memory intensive, but we don't
// keep a reference to the content around so this should be garbage collected. Re-evaluate?
var content = fs.readFileSync(absolutePath);
var sha = crypto.createHash('sha256').update(content).digest('hex');

var resource = percyClient.makeResource({
resourceUrl: encodeURI(resourceUrl),
sha: sha,
localPath: absolutePath,
});

hashToResource[sha] = resource;
next();
}
}
};
walk.walkSync(buildDir, walkOptions);

return hashToResource;
}

// Helper method to parse missing-resources from an API response.
function parseMissingResources(response) {
Expand All @@ -104,7 +44,6 @@ function handlePercyFailure(error) {
// TODO: refactor to break down into a more modular design with less global state.
var percyClient;
var percyConfig;
var normalizedRootUrl;
var percyBuildPromise;
var buildResourceUploadPromises = [];
var snapshotResourceUploadPromises = [];
Expand Down Expand Up @@ -169,8 +108,8 @@ module.exports = {
config: function(env, baseConfig) {
percyConfig = baseConfig.percy || {};

// Store the Ember rootURL without a trailing slash, or a blank string.
normalizedRootUrl = (baseConfig.rootURL || '/').replace(/\/$/, '');
// Store the Ember rootURL to be used later.
percyConfig.baseUrlPath = baseConfig.rootURL;

// Make sure the percy config has a 'breakpoints' object.
percyConfig.breakpointsConfig = percyConfig.breakpointsConfig || {};
Expand Down Expand Up @@ -240,10 +179,9 @@ module.exports = {

if (!isPercyEnabled) { return; }

var hashToResource = gatherBuildResources(percyClient, buildOutputDirectory);
var resources = [];
Object.keys(hashToResource).forEach(function(key) {
resources.push(hashToResource[key]);
var resources = percyClient.gatherBuildResources(buildOutputDirectory, {
baseUrlPath: percyConfig.baseUrlPath,
skippedPathRegexes: SKIPPED_ASSETS,
});

// Initialize the percy client and a new build.
Expand All @@ -261,6 +199,13 @@ module.exports = {
var missingResources = parseMissingResources(buildResponse);
if (missingResources && missingResources.length > 0) {

// Note that duplicate resources with the same SHA will get clobbered here into this
// hash, but that is ok since we only use this to access the content below for upload.
var hashToResource = {};
resources.forEach(function(resource) {
hashToResource[resource.sha] = resource;
});

var missingResourcesIndex = 0;
var promiseGenerator = function() {
var missingResource = missingResources[missingResourcesIndex];
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -58,7 +58,7 @@
"body-parser": "^1.15.0",
"ember-cli-babel": "^6.10.0",
"es6-promise-pool": "^2.4.1",
"percy-client": "^2.8.0",
"percy-client": "^2.10.0",
"walk": "^2.3.9"
},
"ember-addon": {
Expand Down
Binary file added tests/dummy/public/images/identical-image-1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/dummy/public/images/identical-image-2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions tests/integration/components/dummy-box-test.js
Expand Up @@ -83,4 +83,17 @@ module('Integration | Component | dummy box', function(hooks) {

percySnapshot('textarea with value');
});

test('it handles identical assets with different paths', async function(assert) {
await render(hbs`
{{#dummy-box}}
This box should have two identical images below:
<img src="/test-root-url/images/identical-image-1.png">
<img src="/test-root-url/images/identical-image-2.png">
{{/dummy-box}}
`);

percySnapshot('dummy box test with identical assets');
assert.ok(true);
});
});

0 comments on commit ac32ea6

Please sign in to comment.