Skip to content

Commit

Permalink
fix: publish draft by default
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `throwOnFailure` is no longer a supported option. Errors will always be thrown if the publish fails.
Closes #4
  • Loading branch information
bcanseco committed Oct 26, 2018
1 parent bafe55b commit 2b0decf
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 61 deletions.
14 changes: 0 additions & 14 deletions README.md
Expand Up @@ -99,20 +99,6 @@ new PublishExtensionPlugin({
})
```

### `throwOnFailure`

Type: `Boolean`
Default: `false`

By default, the plugin only logs any errors encountered while publishing. Set this to `true` to additionally throw an error.

```js
// in your webpack.config.js
new PublishExtensionPlugin({
throwOnFailure: true,
})
```

### `keepBundleOnSuccess`

Type: `Boolean`
Expand Down
47 changes: 36 additions & 11 deletions src/index.js
Expand Up @@ -21,7 +21,6 @@ export default class PublishExtensionPlugin {
* @param {String} [options.refreshToken] Google OAuth 2.0 refresh token
* @param {String} [options.path] Path to a directory containing a manifest.json file.
* If omitted, webpack's output.path directory will be used.
* @param {Boolean} [options.throwOnFailure=false] Set true to throw an error if publishing is unsuccessful.
* @param {Boolean} [options.keepBundleOnSuccess=false] Set true to keep the ZIP if publishing is successful.
* @param {Boolean} [options.silent=false] Set true to suppress logging
* @param {Boolean} [options.disabled=false] Set true to disable the plugin (same as not having it).
Expand Down Expand Up @@ -52,9 +51,9 @@ export default class PublishExtensionPlugin {
afterEmit = async (compilation) => {
const {path = compilation.outputOptions.path} = this.options;
const bundle = await this.makeBundle(path);
const isPublishSuccess = await this.publish(bundle);
await this.publish(bundle);

if (isPublishSuccess && !this.options.keepBundleOnSuccess) {
if (!this.options.keepBundleOnSuccess) {
unlinkSync(bundle);
}
}
Expand All @@ -77,7 +76,6 @@ export default class PublishExtensionPlugin {
/**
* Publishes a new zipped extension to the Chrome Web Store.
* @param {String} bundle path to a ZIP file containing the extension.
* @return {Promise<Boolean>} True if the publish succeeded, false otherwise.
*/
publish = async (bundle) => {
const {
Expand All @@ -88,18 +86,45 @@ export default class PublishExtensionPlugin {
} = this.options;

const webstore = new ChromeWebStoreClient({extensionId, clientId, clientSecret, refreshToken});
const token = await webstore.fetchToken();
const zipFile = createReadStream(bundle);
const {uploadState, itemError} = await webstore.uploadExisting(zipFile);

await this.uploadZip(webstore, token, zipFile);
await this.publishDraft(webstore, token);
}

/**
* Uploads a new zipped extension as a draft version that still needs to be published.
* @param {ChromeWebStoreClient} webstore Chrome Web Store client used for uploading.
* @param {String} token OAuth 2.0 token to authenticate with.
* @param {ReadStream} zipFile The zip file to use for the upload.
* @throws when the upload fails
*/
uploadZip = async (webstore, token, zipFile) => {
const {uploadState, itemError} = await webstore.uploadExisting(zipFile, token);

if (['SUCCESS', 'IN_PROGRESS'].includes(uploadState)) {
this.log.info(`Published new extension version (${uploadState}).`);
return true;
this.log.info(`Uploaded zipped extension (${uploadState}).`);
} else {
itemError.forEach((err) => this.log.error(`${err.error_code}: ${err.error_detail}`));
if (this.options.throwOnFailure) {
throw new Error('Failed to publish extension.');
}
return false;
throw new Error('Failed to upload zipped extension.');
}
}

/**
* Publishes the current draft as a new extension version.
* @param {ChromeWebStoreClient} webstore Chrome Web Store client used for uploading.
* @param {String} token OAuth 2.0 token to authenticate with.
* @throws when the publish fails
*/
publishDraft = async (webstore, token) => {
const {status: statuses, statusDetail: details} = await webstore.publish('default', token);

if (statuses.includes('OK')) {
this.log.info('Published new extension version.');
} else {
statuses.forEach((status, i) => this.log.error(`${status}: ${details[i]}`));
throw new Error('Failed to publish extension.');
}
}
}
88 changes: 55 additions & 33 deletions src/index.spec.js
Expand Up @@ -2,8 +2,14 @@ import fs, {existsSync, unlinkSync} from 'fs';
import Plugin from '.';
import ChromeWebstoreUpload from 'chrome-webstore-upload';

const mockToken = jest.fn();
const mockUpload = jest.fn();
jest.mock('chrome-webstore-upload', () => jest.fn(() => ({uploadExisting: mockUpload})));
const mockPublish = jest.fn();
jest.mock('chrome-webstore-upload', () => jest.fn(() => ({
fetchToken: mockToken,
uploadExisting: mockUpload,
publish: mockPublish,
})));

describe('constructor', () => {
it('should reject unknown options', () => {
Expand Down Expand Up @@ -41,14 +47,42 @@ describe('makeBundle', () => {
});
});

describe('publish', () => {
beforeAll(() => {
fs.createReadStream = jest.fn().mockImplementation();
describe('uploadZip', () => {
it('should log info if successful', async () => {
const plugin = new Plugin({silent: true});
plugin.log.info = jest.fn();
mockUpload.mockImplementation(() => ({uploadState: 'SUCCESS'}));

await plugin.uploadZip(new ChromeWebstoreUpload(), 'token', 'C:\\fakepathtobundle.zip');

expect(plugin.log.info).toHaveBeenCalled();
});

it('should log and throw error if unsuccessful', async () => {
const plugin = new Plugin({silent: true});
plugin.log.error = jest.fn();
mockUpload.mockImplementation(() => ({
uploadState: 'FAILURE',
itemError: [{error_code: '404', error_detail: 'extension not found'}],
}));

try {
await plugin.uploadZip(new ChromeWebstoreUpload(), 'token', 'C:\\fakepathtobundle.zip');
} catch (err) {
expect(plugin.log.error).toHaveBeenCalled();
}
});
});

describe('publish', () => {
beforeAll(() => fs.createReadStream = jest.fn().mockImplementation());
afterAll(() => process.env = {});

it('should work with process.env vars', async () => {
const plugin = new Plugin({silent: true});
mockUpload.mockImplementation(() => ({uploadState: 'IN_PROGRESS'}));
plugin.uploadZip = jest.fn();
plugin.publishDraft = jest.fn();

process.env = {
GOOGLE_EXTENSION_ID: 'hey',
GOOGLE_CLIENT_ID: 'thats',
Expand All @@ -64,47 +98,32 @@ describe('publish', () => {
clientSecret: 'pretty',
refreshToken: 'good',
});

process.env = {};
});
});

it('should log info and return true if successful', async () => {
describe('publishDraft', () => {
it('should log info if successful', async () => {
const plugin = new Plugin({silent: true});
plugin.log.info = jest.fn();
mockUpload.mockImplementation(() => ({uploadState: 'SUCCESS'}));
mockPublish.mockImplementation(() => ({status: ['OK'], statusDetail: ['OK.']}));

const result = await plugin.publish('C:\\fakepathtobundle.zip');
await plugin.publishDraft(new ChromeWebstoreUpload(), 'token');

expect(result).toBeTruthy();
expect(plugin.log.info).toHaveBeenCalled();
});

it('should log error and return false if unsuccessful', async () => {
it('should log and throw error if unsuccessful', async () => {
const plugin = new Plugin({silent: true});
plugin.log.error = jest.fn();
mockUpload.mockImplementation(() => ({
uploadState: 'FAILURE',
itemError: [{error_code: '404', error_detail: 'extension not found'}],
}));

const result = await plugin.publish('C:\\fakepathtobundle.zip');

expect(result).toBeFalsy();
expect(plugin.log.error).toHaveBeenCalled();
});

it('should throw error if unsuccessful and options.throwOnFailure is true', async () => {
const plugin = new Plugin({silent: true, throwOnFailure: true});
plugin.log.error = jest.fn();
mockUpload.mockImplementation(() => ({
uploadState: 'FAILURE',
itemError: [{error_code: '404', error_detail: 'extension not found'}],
mockPublish.mockImplementation(() => ({
status: ['ITEM_PENDING_REVIEW'],
statusDetail: ['foobar'],
}));

try {
await plugin.publish('C:\\fakepathtobundle.zip');
await plugin.publishDraft(new ChromeWebstoreUpload(), 'token');
} catch (err) {
expect(err).toBeInstanceOf(Error);
expect(plugin.log.error).toHaveBeenCalled();
}
});
});
Expand All @@ -131,6 +150,10 @@ describe('apply', () => {
});

describe('afterEmit', () => {
fs.unlinkSync = jest.fn();

beforeEach(fs.unlinkSync.mockReset);

it('should use options.path if provided', async () => {
const plugin = new Plugin({path: 'c:\\fakepath'});
plugin.makeBundle = jest.fn();
Expand All @@ -153,9 +176,8 @@ describe('afterEmit', () => {

it('should not delete zip if options.keepBundleOnSuccess is true', async () => {
const plugin = new Plugin({keepBundleOnSuccess: true, path: 'c:\\fakepath'});
fs.unlinkSync = jest.fn();
plugin.makeBundle = jest.fn();
plugin.publish = jest.fn(() => Promise.resolve(true));
plugin.publish = jest.fn();

await plugin.afterEmit();

Expand Down
3 changes: 0 additions & 3 deletions src/options.json
Expand Up @@ -16,9 +16,6 @@
"path": {
"type": "string"
},
"throwOnFailure": {
"type": "boolean"
},
"keepBundleOnSuccess": {
"type": "boolean"
},
Expand Down

0 comments on commit 2b0decf

Please sign in to comment.