Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix duplicate asset clobbering resource paths. #70

Merged
merged 5 commits into from Jul 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
});
});