use native promises

This commit is contained in:
Joao Moreno 2015-10-13 16:49:47 +02:00
parent 2e91f67b9c
commit 0493ffd5a3
9 changed files with 193 additions and 63 deletions

View file

@ -27,15 +27,15 @@
"prepublish": "gulp compile"
},
"engines": {
"node": ">= 0.10.3"
"node": ">= 0.12"
},
"dependencies": {
"commander": "^2.8.1",
"denodeify": "^1.2.1",
"glob": "^5.0.14",
"lodash": "^3.10.1",
"minimatch": "^2.0.10",
"osenv": "^0.1.3",
"q": "^1.4.1",
"read": "^1.0.7",
"tmp": "0.0.27",
"vso-node-api": "^0.4.2",

View file

@ -1,12 +1,17 @@
import * as fs from 'fs';
import * as path from 'path';
import * as cp from 'child_process';
import * as _ from 'lodash';
import * as yazl from 'yazl';
import { Manifest } from './manifest';
import { nfcall, Promise, reject, resolve, all } from 'q';
import * as glob from 'glob';
import * as _glob from 'glob';
import * as minimatch from 'minimatch';
import { exec } from 'child_process';
import * as denodeify from 'denodeify';
const readFile = denodeify<string, string, string>(fs.readFile);
const unlink = denodeify<string, void>(fs.unlink);
const exec = denodeify<string, { cwd?: string; }, { stdout: string; stderr: string; }>(cp.exec, (err, stdout, stderr) => [err, { stdout, stderr }]);
const glob = denodeify<string, _glob.IOptions, string[]>(_glob);
const resourcesPath = path.join(path.dirname(__dirname), 'resources');
const vsixManifestTemplatePath = path.join(resourcesPath, 'extension.vsixmanifest');
@ -24,38 +29,38 @@ export interface IPackageResult {
function validateManifest(manifest: Manifest): Promise<Manifest> {
if (!manifest.publisher) {
return reject<Manifest>('Manifest missing field: publisher');
return Promise.reject('Manifest missing field: publisher');
}
if (!manifest.name) {
return reject<Manifest>('Manifest missing field: name');
return Promise.reject('Manifest missing field: name');
}
if (!manifest.version) {
return reject<Manifest>('Manifest missing field: version');
return Promise.reject('Manifest missing field: version');
}
if (!manifest.engines) {
return reject<Manifest>('Manifest missing field: engines');
return Promise.reject('Manifest missing field: engines');
}
if (!manifest.engines.vscode) {
return reject<Manifest>('Manifest missing field: engines.vscode');
return Promise.reject('Manifest missing field: engines.vscode');
}
return resolve(manifest);
return Promise.resolve(manifest);
}
export function readManifest(cwd: string): Promise<Manifest> {
const manifestPath = path.join(cwd, 'package.json');
return nfcall<string>(fs.readFile, manifestPath, 'utf8')
.catch(() => reject<string>(`Extension manifest not found: ${ manifestPath }`))
return readFile(manifestPath, 'utf8')
.catch(() => Promise.reject(`Extension manifest not found: ${ manifestPath }`))
.then<Manifest>(manifestStr => {
try {
return resolve(JSON.parse(manifestStr));
return Promise.resolve(JSON.parse(manifestStr));
} catch (e) {
return reject(`Error parsing manifest file: not a valid JSON file.`);
return Promise.reject(`Error parsing manifest file: not a valid JSON file.`);
}
})
.then(validateManifest);
@ -63,22 +68,22 @@ export function readManifest(cwd: string): Promise<Manifest> {
function prepublish(cwd: string, manifest: Manifest): Promise<Manifest> {
if (!manifest.scripts || !manifest.scripts['vscode:prepublish']) {
return resolve(manifest);
return Promise.resolve(manifest);
}
const script = manifest.scripts['vscode:prepublish'];
console.warn(`Executing prepublish script '${ script }'...`);
return nfcall<string>(exec, script, { cwd })
.catch(err => reject(err.message))
.spread((stdout: string, stderr: string) => {
return exec(script, { cwd })
.then(({ stdout }) => {
process.stdout.write(stdout);
return resolve(manifest);
});
return Promise.resolve(manifest);
})
.catch(err => Promise.reject(err.message));
}
function toVsixManifest(manifest: Manifest): Promise<string> {
return nfcall<string>(fs.readFile, vsixManifestTemplatePath, 'utf8')
return readFile(vsixManifestTemplatePath, 'utf8')
.then(vsixManifestTemplateStr => _.template(vsixManifestTemplateStr))
.then(vsixManifestTemplate => vsixManifestTemplate({
id: manifest.name,
@ -103,21 +108,22 @@ function devDependenciesIgnore(manifest: Manifest): string[] {
}
function collectFiles(cwd: string, manifest: Manifest): Promise<string[]> {
return nfcall<string[]>(glob, '**', { cwd, nodir: true, dot: true }).then(files => {
return nfcall<string>(fs.readFile, path.join(cwd, '.vscodeignore'), 'utf8')
.catch<string>(err => err.code !== 'ENOENT' ? reject(err) : resolve(''))
return glob('**', { cwd, nodir: true, dot: true }).then(files => {
return readFile(path.join(cwd, '.vscodeignore'), 'utf8')
.catch<string>(err => err.code !== 'ENOENT' ? Promise.reject(err) : Promise.resolve(''))
.then(rawIgnore => rawIgnore.split(/[\n\r]/).map(s => s.trim()).filter(s => !!s))
.then(ignore => devDependenciesIgnore(manifest).concat(ignore))
.then(ignore => defaultIgnore.concat(ignore))
.then(ignore => ignore.filter(i => !/^\s*#/.test(i)))
.then(ignore => _.partition(ignore, i => !/^\s*!/.test(i)))
.spread((ignore: string[], negate: string[]) => files.filter(f => !ignore.some(i => minimatch(f, i)) || negate.some(i => minimatch(f, i.substr(1)))));
.then<{ ignore: string[]; negate: string[]; }>(ignore => <any> _.indexBy(_.partition(ignore, i => !/^\s*!/.test(i)), (o, i) => i ? 'negate' : 'ignore'))
.then(({ ignore, negate }) => files.filter(f => !ignore.some(i => minimatch(f, i)) || negate.some(i => minimatch(f, i.substr(1)))));
});
}
export function collect(cwd: string, manifest: Manifest): Promise<IFile[]> {
return all<any>([toVsixManifest(manifest), collectFiles(cwd, manifest)])
.spread((vsixManifest: string, files: string[]) => [
return Promise.all<any>([toVsixManifest(manifest), collectFiles(cwd, manifest)])
.then<{ vsixManifest: string; files: string[]; }>(promises => <any> _.indexBy(promises, (o,i) => i ? 'files' : 'vsixManifest'))
.then(({ vsixManifest, files }) => [
{ path: 'extension.vsixmanifest', contents: new Buffer(vsixManifest, 'utf8') },
{ path: '[Content_Types].xml', localPath: path.join(resourcesPath, '[Content_Types].xml') },
...files.map(f => ({ path: `extension/${ f }`, localPath: path.join(cwd, f) }))
@ -125,9 +131,9 @@ export function collect(cwd: string, manifest: Manifest): Promise<IFile[]> {
}
function writeVsix(files: IFile[], packagePath: string): Promise<string> {
return nfcall(fs.unlink, packagePath)
.catch(err => err.code !== 'ENOENT' ? reject(err) : resolve(null))
.then(() => Promise<string>((c, e) => {
return unlink(packagePath)
.catch(err => err.code !== 'ENOENT' ? Promise.reject(err) : Promise.resolve(null))
.then(() => new Promise<string>((c, e) => {
const zip = new yazl.ZipFile();
files.forEach(f => f.contents ? zip.addBuffer(f.contents, f.path) : zip.addFile(f.localPath, f.path));
zip.end();

View file

@ -1,17 +1,20 @@
import { readFile } from 'fs';
import * as fs from 'fs';
import { ExtensionQueryFlags, PublishedExtension, ExtensionQueryFilterType, PagingDirection } from 'vso-node-api/interfaces/GalleryInterfaces';
import { nfcall, Promise, reject, ninvoke, resolve } from 'q';
import { pack, readManifest } from './package';
import { tmpName } from 'tmp';
import * as tmp from 'tmp';
import { getPublisher } from './store';
import { getGalleryAPI, getRawGalleryAPI, read } from './util';
import { validatePublisher } from './validation';
import { Manifest } from './manifest';
import * as denodeify from 'denodeify';
const tmpName = denodeify<string>(tmp.tmpName);
const readFile = denodeify<string, string, string>(fs.readFile);
const galleryUrl = 'https://app.market.visualstudio.com';
export function publish(cwd = process.cwd()): Promise<any> {
return nfcall<string>(tmpName)
return tmpName()
.then(packagePath => pack(packagePath, cwd))
.then(result => {
const { manifest, packagePath } = result;
@ -20,15 +23,15 @@ export function publish(cwd = process.cwd()): Promise<any> {
.then(p => p.pat)
.then(getGalleryAPI)
.then(api => {
return nfcall<string>(readFile, packagePath, 'base64').then(extensionManifest => {
return readFile(packagePath, 'base64').then(extensionManifest => {
const fullName = `${ manifest.publisher}.${ manifest.name }@${ manifest.version }`;
console.log(`Publishing ${ fullName }...`);
return api.getExtension(manifest.publisher, manifest.name, null, ExtensionQueryFlags.IncludeVersions)
.catch<PublishedExtension>(err => err.statusCode === 404 ? null : reject(err))
.catch<PublishedExtension>(err => err.statusCode === 404 ? null : Promise.reject(err))
.then(extension => {
if (extension && extension.versions.some(v => v.version === manifest.version)) {
return reject<void>(`${ fullName } already exists.`);
return Promise.reject(`${ fullName } already exists.`);
}
var promise = extension
@ -36,7 +39,7 @@ export function publish(cwd = process.cwd()): Promise<any> {
: api.createExtension({ extensionManifest });
return promise
.catch(err => reject(err.statusCode === 409 ? `${ fullName } already exists.` : err))
.catch(err => Promise.reject(err.statusCode === 409 ? `${ fullName } already exists.` : err))
.then(() => console.log(`Successfully published ${ fullName }!`));
});
});
@ -65,18 +68,21 @@ export function list(publisher: string): Promise<any> {
export function unpublish(publisher?: string, name?: string, cwd = process.cwd()): Promise<any> {
const details = publisher && name
? resolve(({ publisher, name }))
? Promise.resolve(({ publisher, name }))
: readManifest(cwd);
return details.then(({ publisher, name }) => {
const fullName = `${ publisher }.${ name }`;
return read(`This will FOREVER delete '${ fullName }'! Are you sure? [y/N] `)
.then(answer => /^y$/i.test(answer) ? null : reject('Aborted'))
.then(answer => /^y$/i.test(answer) ? null : Promise.reject('Aborted'))
.then(() => getPublisher(publisher))
.then(p => p.pat)
.then(getRawGalleryAPI)
.then(api => ninvoke(api, 'deleteExtension', publisher, name, ''))
.then(api => {
const deleteExtension = denodeify<string, string, string, void>(api.deleteExtension.bind(api));
return deleteExtension(publisher, name, '');
})
.then(() => console.log(`Successfully deleted ${ fullName }!`));
});
}

View file

@ -1,12 +1,15 @@
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
import { Promise, nfcall, resolve, reject, ninvoke } from 'q';
import * as cp from 'child_process';
import { home } from 'osenv';
import { read, getGalleryAPI, getRawGalleryAPI } from './util';
import { validatePublisher } from './validation';
import * as denodeify from 'denodeify';
const readFile = denodeify<string, string, string>(fs.readFile);
const writeFile = denodeify<string, string, void>(fs.writeFile);
const storePath = path.join(home(), '.vsce');
const exec = denodeify<string, { stdout: string; stderr: string; }>(cp.exec, (err, stdout, stderr) => [err, { stdout, stderr }]);
export interface IPublisher {
name: string;
@ -23,29 +26,29 @@ export interface IGetOptions {
}
function load(): Promise<IStore> {
return nfcall<string>(fs.readFile, storePath, 'utf8')
.catch<string>(err => err.code !== 'ENOENT' ? reject(err) : resolve('{}'))
return readFile(storePath, 'utf8')
.catch<string>(err => err.code !== 'ENOENT' ? Promise.reject(err) : Promise.resolve('{}'))
.then<IStore>(rawStore => {
try {
return resolve(JSON.parse(rawStore));
return Promise.resolve(JSON.parse(rawStore));
} catch (e) {
return reject(`Error parsing store: ${ storePath }`);
return Promise.reject(`Error parsing store: ${ storePath }`);
}
})
.then(store => {
store.publishers = store.publishers || [];
return resolve(store);
return Promise.resolve(store);
});
}
function save(store: IStore): Promise<IStore> {
return nfcall<void>(fs.writeFile, storePath, JSON.stringify(store))
return writeFile(storePath, JSON.stringify(store))
.then(() => {
if (process.platform !== 'win32') {
return resolve(null);
return Promise.resolve(null);
}
return nfcall(exec, `attrib +H ${ storePath }`);
return exec(`attrib +H ${ storePath }`);
})
.then(() => store);
}
@ -78,7 +81,7 @@ export function getPublisher(publisherName: string): Promise<IPublisher> {
return load().then(store => {
const publisher = store.publishers.filter(p => p.name === publisherName)[0];
return publisher ? resolve(publisher) : requestPAT(store, publisherName);
return publisher ? Promise.resolve(publisher) : requestPAT(store, publisherName);
});
}
@ -92,10 +95,10 @@ export function loginPublisher(publisherName: string): Promise<IPublisher> {
if (publisher) {
console.log(`Publisher '${ publisherName }' is already known`);
return read('Do you want to overwrite its PAT? [y/N] ')
.then(answer => /^y$/i.test(answer) ? store : reject('Aborted'));
.then(answer => /^y$/i.test(answer) ? store : Promise.reject('Aborted'));
}
return resolve(store);
return Promise.resolve(store);
})
.then(store => requestPAT(store, publisherName));
}
@ -107,7 +110,7 @@ export function logoutPublisher(publisherName: string): Promise<any> {
const publisher = store.publishers.filter(p => p.name === publisherName)[0];
if (!publisher) {
return reject(`Unknown publisher '${ publisherName }'`);
return Promise.reject(`Unknown publisher '${ publisherName }'`);
}
return removePublisherFromStore(store, publisherName);
@ -141,8 +144,12 @@ export function createPublisher(publisherName: string): Promise<any> {
export function deletePublisher(publisherName: string): Promise<any> {
return getPublisher(publisherName).then(({ pat }) => {
return read(`This will FOREVER delete '${ publisherName }'! Are you sure? [y/N] `)
.then(answer => /^y$/i.test(answer) ? null : reject('Aborted'))
.then(() => ninvoke(getRawGalleryAPI(pat), 'deletePublisher', publisherName))
.then(answer => /^y$/i.test(answer) ? null : Promise.reject('Aborted'))
.then(() => {
const rawApi = getRawGalleryAPI(pat);
const deletePublisher = denodeify<string, void>(rawApi.deletePublisher.bind(rawApi));
return deletePublisher(publisherName);
})
.then(() => load().then(store => removePublisherFromStore(store, publisherName)))
.then(() => console.log(`Successfully deleted publisher '${ publisherName }'.`));
});

View file

@ -1,8 +1,8 @@
import { Promise, nfcall } from 'q';
import { assign } from 'lodash';
import _read = require('read');
import * as _read from 'read';
import { WebApi, getBasicHandler } from 'vso-node-api/WebApi';
import { IGalleryApi, IQGalleryApi } from 'vso-node-api/GalleryApi';
import * as denodeify from 'denodeify';
export function fatal(message: any, ...args: any[]) {
if (message instanceof Error) {
@ -17,9 +17,9 @@ export function fatal(message: any, ...args: any[]) {
process.exit(1);
}
const __read = denodeify<_read.Options,string>(_read);
export function read(prompt: string, options: _read.Options = {}): Promise<string> {
return nfcall<string>(_read, assign({ prompt }, options))
.spread(r => r);
return __read(assign({ prompt }, options));
}
export function getGalleryAPI(pat: string): IQGalleryApi {

View file

@ -28,6 +28,9 @@
},
"commander/commander.d.ts": {
"commit": "14ab703f435375194360cdba3abab61afc879c18"
},
"es6-promise/es6-promise.d.ts": {
"commit": "14ab703f435375194360cdba3abab61afc879c18"
}
}
}

34
typings/denodeify/denodeify.d.ts vendored Normal file
View file

@ -0,0 +1,34 @@
// Type definitions for denodeify 1.2.1
// Project: https://github.com/matthew-andrews/denodeify
// Definitions by: joaomoreno <https://github.com/joaomoreno/>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
declare module "denodeify" {
function _<R>(fn: _.F0<R>, transformer?: _.M): () => Promise<R>;
function _<A,R>(fn: _.F1<A,R>, transformer?: _.M): (a:A) => Promise<R>;
function _<A,B,R>(fn: _.F2<A,B,R>, transformer?: _.M): (a:A, b:B) => Promise<R>;
function _<A,B,C,R>(fn: _.F3<A,B,C,R>, transformer?: _.M): (a:A, b:B, c:C) => Promise<R>;
function _<A,B,C,D,R>(fn: _.F4<A,B,C,D,R>, transformer?: _.M): (a:A, b:B, c:C, d:D) => Promise<R>;
function _<A,B,C,D,E,R>(fn: _.F5<A,B,C,D,E,R>, transformer?: _.M): (a:A, b:B, c:C, d:D, e:E) => Promise<R>;
function _<A,B,C,D,E,F,R>(fn: _.F6<A,B,C,D,E,F,R>, transformer?: _.M): (a:A, b:B, c:C, d:D, e:E, f:F) => Promise<R>;
function _<A,B,C,D,E,F,G,R>(fn: _.F7<A,B,C,D,E,F,G,R>, transformer?: _.M): (a:A, b:B, c:C, d:D, e:E, f:F, g:G) => Promise<R>;
function _<A,B,C,D,E,F,G,H,R>(fn: _.F8<A,B,C,D,E,F,G,H,R>, transformer?: _.M): (a:A, b:B, c:C, d:D, e:E, f:F, g:G, h:H) => Promise<R>;
function _(fn: _.F, transformer?: _.M): (...args) => Promise<any>;
module _ {
type Callback<R> = (err: Error, result: R) => any;
type F0<R> = (cb: Callback<R>) => any;
type F1<A,R> = (a:A, cb: Callback<R>) => any;
type F2<A,B,R> = (a:A, b:B, cb: Callback<R>) => any;
type F3<A,B,C,R> = (a:A, b:B, c:C, cb: Callback<R>) => any;
type F4<A,B,C,D,R> = (a:A, b:B, c:C, d:D, cb: Callback<R>) => any;
type F5<A,B,C,D,E,R> = (a:A, b:B, c:C, d:D, e:E, cb: Callback<R>) => any;
type F6<A,B,C,D,E,F,R> = (a:A, b:B, c:C, d:D, e:E, f:F, cb: Callback<R>) => any;
type F7<A,B,C,D,E,F,G,R> = (a:A, b:B, c:C, d:D, e:E, f:F, g:G, cb: Callback<R>) => any;
type F8<A,B,C,D,E,F,G,H,R> = (a:A, b:B, c:C, d:D, e:E, f:F, g:G, h:H, cb: Callback<R>) => any;
type F = (...args) => any;
type M = (err: Error, ...args) => any[];
}
export = _;
}

73
typings/es6-promise/es6-promise.d.ts vendored Normal file
View file

@ -0,0 +1,73 @@
// Type definitions for es6-promise
// Project: https://github.com/jakearchibald/ES6-Promise
// Definitions by: François de Campredon <https://github.com/fdecampredon/>, vvakame <https://github.com/vvakame>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
interface Thenable<R> {
then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => void): Thenable<U>;
}
declare class Promise<R> implements Thenable<R> {
/**
* If you call resolve in the body of the callback passed to the constructor,
* your promise is fulfilled with result object passed to resolve.
* If you call reject your promise is rejected with the object passed to resolve.
* For consistency and debugging (eg stack traces), obj should be an instanceof Error.
* Any errors thrown in the constructor callback will be implicitly passed to reject().
*/
constructor(callback: (resolve : (value?: R | Thenable<R>) => void, reject: (error?: any) => void) => void);
/**
* onFulfilled is called when/if "promise" resolves. onRejected is called when/if "promise" rejects.
* Both are optional, if either/both are omitted the next onFulfilled/onRejected in the chain is called.
* Both callbacks have a single parameter , the fulfillment value or rejection reason.
* "then" returns a new promise equivalent to the value you return from onFulfilled/onRejected after being passed through Promise.resolve.
* If an error is thrown in the callback, the returned promise rejects with that error.
*
* @param onFulfilled called when/if "promise" resolves
* @param onRejected called when/if "promise" rejects
*/
then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => U | Thenable<U>): Promise<U>;
then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => void): Promise<U>;
/**
* Sugar for promise.then(undefined, onRejected)
*
* @param onRejected called when/if "promise" rejects
*/
catch<U>(onRejected?: (error: any) => U | Thenable<U>): Promise<U>;
}
declare module Promise {
/**
* Make a new promise from the thenable.
* A thenable is promise-like in as far as it has a "then" method.
*/
function resolve<R>(value?: R | Thenable<R>): Promise<R>;
/**
* Make a promise that rejects to obj. For consistency and debugging (eg stack traces), obj should be an instanceof Error
*/
function reject(error: any): Promise<any>;
/**
* Make a promise that fulfills when every item in the array fulfills, and rejects if (and when) any item rejects.
* the array passed to all can be a mixture of promise-like objects and other objects.
* The fulfillment value is an array (in order) of fulfillment values. The rejection value is the first rejection value.
*/
function all<R>(promises: (R | Thenable<R>)[]): Promise<R[]>;
/**
* Make a Promise that fulfills when any item fulfills, and rejects if any item rejects.
*/
function race<R>(promises: (R | Thenable<R>)[]): Promise<R>;
}
declare module 'es6-promise' {
var foo: typeof Promise; // Temp variable to reference Promise in local context
module rsvp {
export var Promise: typeof foo;
}
export = rsvp;
}

1
typings/tsd.d.ts vendored
View file

@ -6,3 +6,4 @@
/// <reference path="vso-node-api/vso-node-api.d.ts" />
/// <reference path="tmp/tmp.d.ts" />
/// <reference path="commander/commander.d.ts" />
/// <reference path="es6-promise/es6-promise.d.ts" />