Skip to content
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"7zip": "0.0.6",
"cross-unzip": "0.0.2",
"rimraf": "^2.5.2",
"semver": "^5.3.0"
"semver": "^5.3.0",
"xml2js": "^0.4.17"
},
"babel": {
"presets": [
Expand Down
46 changes: 26 additions & 20 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'path';
import semver from 'semver';

import downloadChromeExtension from './downloadChromeExtension';
import needUpdate from './needUpdate';
import { getPath } from './utils';

const { BrowserWindow } = remote || electron;
Expand All @@ -14,9 +15,9 @@ if (fs.existsSync(IDMapPath)) {
IDMap = JSON.parse(fs.readFileSync(IDMapPath, 'utf8'));
}

const install = (extensionReference, forceDownload = false) => {
const install = (extensionReference, checkUpdate = false) => {
if (Array.isArray(extensionReference)) {
return Promise.all(extensionReference.map(extension => install(extension, forceDownload)));
return Promise.all(extensionReference.map(extension => install(extension, checkUpdate)));
}
let chromeStoreID;
if (typeof extensionReference === 'object' && extensionReference.id) {
Expand All @@ -35,24 +36,29 @@ const install = (extensionReference, forceDownload = false) => {
const extensionInstalled = extensionName &&
BrowserWindow.getDevToolsExtensions &&
BrowserWindow.getDevToolsExtensions()[extensionName];
if (!forceDownload && extensionInstalled) {
return Promise.resolve(IDMap[chromeStoreID]);
}
return downloadChromeExtension(chromeStoreID, forceDownload)
.then((extensionFolder) => {
// Use forceDownload, but already installed
if (extensionInstalled) {
BrowserWindow.removeDevToolsExtension(extensionName);
}
const name = BrowserWindow.addDevToolsExtension(extensionFolder); // eslint-disable-line
fs.writeFileSync(
IDMapPath,
JSON.stringify(Object.assign(IDMap, {
[chromeStoreID]: name,
})),
);
return Promise.resolve(name);
});
const promise = checkUpdate && extensionInstalled ?
needUpdate(chromeStoreID, extensionInstalled.version) :
Promise.resolve(false);
return promise.then((toUpdate) => {
if (!toUpdate && extensionInstalled) {
return Promise.resolve(IDMap[chromeStoreID]);
}
return downloadChromeExtension(chromeStoreID, toUpdate)
.then((extensionFolder) => {
// Use forceDownload, but already installed
if (extensionInstalled) {
BrowserWindow.removeDevToolsExtension(extensionName);
}
const name = BrowserWindow.addDevToolsExtension(extensionFolder); // eslint-disable-line
fs.writeFileSync(
IDMapPath,
JSON.stringify(Object.assign(IDMap, {
[chromeStoreID]: name,
})),
);
return Promise.resolve(name);
});
});
};

export default install;
Expand Down
23 changes: 23 additions & 0 deletions src/needUpdate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { parseString } from 'xml2js';
import { fetchData } from './utils';

export default (chromeStoreID, currentVersion) => new Promise((resolve, reject) =>
fetchData(
`https://clients2.google.com/service/update2/crx?x=id%3D${chromeStoreID}%26uc&prodversion=32`,
).then((res) => {
if (res.statusCode === 200) {
parseString(res.body, (err, result) => {
const app = result.gupdate.app[0].$;
if (app.status !== 'ok') return resolve(false);

const { status, version: newestVersion } = result.gupdate.app[0].updatecheck[0].$;
if (status !== 'ok') return resolve(false);

if (newestVersion !== currentVersion) return resolve(true);
return resolve(false);
});
} else {
reject(`Failed to check current version of ${chromeStoreID}.`);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should log something and resolve(true) just in case? Rejecting this promise will simply mess with developers, we can try to help them out as much as possible 👍

Copy link
Contributor Author

@jhen0409 jhen0409 May 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the API again, it should always response status 200 if there is no any network errors, so I just remove the line, and log message if response app.status is error-invalidAppId.

}
}),
);
28 changes: 21 additions & 7 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,34 @@ export const getPath = () => {
};

// Use https.get fallback for Electron < 1.4.5
const { net } = (remote || electron);
const { net } = remote || electron;
const request = net ? net.request : https.get;

export const downloadFile = (from, to) => new Promise((resolve, reject) => {
const req = request(from);
const sendRequest = url => new Promise((resolve, reject) => {
const req = request(url);
req.on('response', (res) => {
// Shouldn't handle redirect with `electron.net`, this is for https.get fallback
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return downloadFile(res.headers.location, to)
.then(resolve)
.catch(reject);
return sendRequest(res.headers.location);
}
res.pipe(fs.createWriteStream(to)).on('close', resolve);
resolve(res);
});
req.on('error', reject);
req.end();
});

export const downloadFile = (from, to) => new Promise((resolve, reject) =>
sendRequest(from)
.then(res => res.pipe(fs.createWriteStream(to)).on('close', resolve))
.catch(reject),
);

export const fetchData = url => new Promise((resolve, reject) =>
sendRequest(url)
.then((res) => {
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => resolve(Object.assign(res, { body })));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Purely to avoid mutating what should be an isolated object can we change this to

Object.assign({}, res, { body })

})
.catch(reject),
);
2 changes: 1 addition & 1 deletion test/install_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('Extension Installer', () => {
.catch(() => done('Failed to resolve'));
});

it('should upgraded the extension with forceDownload', (done) => {
it('should upgraded the extension with checkUpdate', (done) => {
const extensionName = 'React Developer Tools';
const oldVersion = '0.14.0';
BrowserWindow.removeDevToolsExtension(extensionName);
Expand Down
19 changes: 19 additions & 0 deletions test/update_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Pre-run
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { given } from 'mocha-testdata';

// Actual Test Imports
import needUpdate from '../src/needUpdate';
import knownExtensions from './testdata/knownExtensions';

chai.use(chaiAsPromised);
chai.should();

describe('Extension Update Checker', () => {
describe('when given a valid extension ID', () => {
given(...knownExtensions).it('should need to update (with different version)', item =>
needUpdate(item.id, '0.0.0').should.become(true),
);
});
});