From ecb98e2e8224a457c439e2da624000555c45f914 Mon Sep 17 00:00:00 2001 From: Max Lynch Date: Mon, 6 Feb 2017 17:15:46 -0600 Subject: [PATCH] Core kind of buidls --- package.json | 1 + scripts/build/build.sh | 1 + scripts/build/tsconfig-core.json | 4 +- src/@ionic-native/core/bootstrap.ts | 3 +- src/@ionic-native/core/plugin.ts | 5 +- src/core/plugin.ts | 566 ++++++++++++++++++++++++++++ 6 files changed, 575 insertions(+), 5 deletions(-) create mode 100644 scripts/build/build.sh create mode 100644 src/core/plugin.ts diff --git a/package.json b/package.json index 683bef3c3..298d4fc48 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "start": "npm run test:watch", "lint": "gulp lint", "build": "npm run lint && npm run build:js && npm run build:esm && npm run build:bundle && npm run build:minify", + "build2:js": "tsc -p scripts/build/tsconfig-core.json", "build:js": "tsc -p tsconfig-es5.json", "build:esm": "tsc -p tsconfig-esm.json", "build:bundle": "browserify dist/es5/index.js > dist/ionic.native.js", diff --git a/scripts/build/build.sh b/scripts/build/build.sh new file mode 100644 index 000000000..c65a4e140 --- /dev/null +++ b/scripts/build/build.sh @@ -0,0 +1 @@ +tsc -p tsconfig-core.json diff --git a/scripts/build/tsconfig-core.json b/scripts/build/tsconfig-core.json index 2b2ca8879..b45d378cc 100644 --- a/scripts/build/tsconfig-core.json +++ b/scripts/build/tsconfig-core.json @@ -7,7 +7,7 @@ "module": "es2015", "moduleResolution": "node", "outDir": "../../dist/packages-dist/core", - "rootDir": ".", + "rootDir": "../../src/", "sourceMap": true, "inlineSources": true, "target": "es5", @@ -15,6 +15,6 @@ "lib": ["es2015", "dom"] }, "files": [ - "index.ts" + "../../src/@ionic-native/core/index.ts" ] } diff --git a/src/@ionic-native/core/bootstrap.ts b/src/@ionic-native/core/bootstrap.ts index ca34d3884..059e6f61e 100644 --- a/src/@ionic-native/core/bootstrap.ts +++ b/src/@ionic-native/core/bootstrap.ts @@ -1,7 +1,8 @@ +declare var window; + export function checkReady() { const DEVICE_READY_TIMEOUT = 5000; - declare var window; // To help developers using cordova, we listen for the device ready event and // log an error if it didn't fire in a reasonable amount of time. Generally, diff --git a/src/@ionic-native/core/plugin.ts b/src/@ionic-native/core/plugin.ts index 4fa4b2ba8..c8610dbfa 100644 --- a/src/@ionic-native/core/plugin.ts +++ b/src/@ionic-native/core/plugin.ts @@ -1,8 +1,9 @@ -import { get } from '../util'; +import { get } from './util'; +import { checkReady } from './bootstrap'; + import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/fromEvent'; -import { checkReady } from '../bootstrap'; checkReady(); diff --git a/src/core/plugin.ts b/src/core/plugin.ts new file mode 100644 index 000000000..814cb76b9 --- /dev/null +++ b/src/core/plugin.ts @@ -0,0 +1,566 @@ +import { get } from '../util'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/fromEvent'; + +import { checkReady } from './bootstrap'; + +checkReady(); + +declare var window; +declare var Promise; + +/** + * @private + */ +export interface PluginConfig { + /** + * Plugin name, this should match the class name + */ + pluginName: string; + /** + * Plugin NPM package name + */ + plugin: string; + /** + * Plugin object reference + */ + pluginRef: string; + /** + * Github repository URL + */ + repo: string; + /** + * Custom install command + */ + install?: string; + /** + * Supported platforms + */ + platforms?: string[]; +} + +/** + * @private + */ +export interface CordovaOptions { + /** + * Set to true if the wrapped method is a sync function + */ + sync?: boolean; + /** + * Callback order. Set to reverse if the success/error callbacks are the first 2 arguments that the wrapped method takes. + */ + callbackOrder?: 'reverse'; + /** + * Callback style + */ + callbackStyle?: 'node' | 'object'; + /** + * Set a custom index for the success callback function. This doesn't work if callbackOrder or callbackStyle are set. + */ + successIndex?: number; + /** + * Set a custom index for the error callback function. This doesn't work if callbackOrder or callbackStyle are set. + */ + errorIndex?: number; + /** + * Success function property name. This must be set if callbackStyle is set to object. + */ + successName?: string; + /** + * Error function property name. This must be set if callbackStyle is set to object. + */ + errorName?: string; + /** + * Set to true to return an observable + */ + observable?: boolean; + /** + * If observable is set to true, this can be set to a different function name that will cancel the observable. + */ + clearFunction?: string; + /** + * This can be used if clearFunction is set. Set this to true to call the clearFunction with the same arguments used in the initial function. + */ + clearWithArgs?: boolean; + /** + * Creates an observable that wraps a global event. Replaces document.addEventListener + */ + eventObservable?: boolean; + /** + * Event name, this must be set if eventObservable is set to true + */ + event?: string; + /** + * Element to attach the event listener to, this is optional, defaults to `window` + */ + element?: any; + /** + * Set to true if the wrapped method returns a promise + */ + otherPromise?: boolean; + /** + * Supported platforms + */ + platforms?: string[]; +} + +/** + * @private + * @param pluginRef + * @returns {null|*} + */ +export const getPlugin = function(pluginRef: string): any { + return get(window, pluginRef); +}; + +/** + * @private + * @param pluginObj + * @param method + */ +export const pluginWarn = function(pluginObj: any, method?: string) { + let pluginName = pluginObj.pluginName, plugin = pluginObj.plugin; + if (method) { + console.warn('Native: tried calling ' + pluginName + '.' + method + ', but the ' + pluginName + ' plugin is not installed.'); + } else { + console.warn('Native: tried accessing the ' + pluginName + ' plugin but it\'s not installed.'); + } + console.warn('Install the ' + pluginName + ' plugin: \'ionic plugin add ' + plugin + '\''); +}; + +/** + * @private + * @param pluginName + * @param method + */ +export const cordovaWarn = function(pluginName: string, method: string) { + if (method) { + console.warn('Native: tried calling ' + pluginName + '.' + method + ', but Cordova is not available. Make sure to include cordova.js or run in a device/simulator'); + } else { + console.warn('Native: tried accessing the ' + pluginName + ' plugin but Cordova is not available. Make sure to include cordova.js or run in a device/simulator'); + } +}; +function setIndex(args: any[], opts: any = {}, resolve?: Function, reject?: Function): any { + // ignore resolve and reject in case sync + if (opts.sync) { + return args; + } + + // If the plugin method expects myMethod(success, err, options) + if (opts.callbackOrder === 'reverse') { + // Get those arguments in the order [resolve, reject, ...restOfArgs] + args.unshift(reject); + args.unshift(resolve); + } else if (opts.callbackStyle === 'node') { + args.push((err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + } else if (opts.callbackStyle === 'object' && opts.successName && opts.errorName) { + let obj: any = {}; + obj[opts.successName] = resolve; + obj[opts.errorName] = reject; + args.push(obj); + } else if (typeof opts.successIndex !== 'undefined' || typeof opts.errorIndex !== 'undefined') { + const setSuccessIndex = () => { + // If we've specified a success/error index + if (opts.successIndex > args.length) { + args[opts.successIndex] = resolve; + } else { + args.splice(opts.successIndex, 0, resolve); + } + }; + + const setErrorIndex = () => { + // We don't want that the reject cb gets spliced into the position of an optional argument that has not been defined and thus causing non expected behaviour. + if (opts.errorIndex > args.length) { + args[opts.errorIndex] = reject; // insert the reject fn at the correct specific index + } else { + args.splice(opts.errorIndex, 0, reject); // otherwise just splice it into the array + } + }; + + if (opts.successIndex > opts.errorIndex) { + setErrorIndex(); + setSuccessIndex(); + } else { + setSuccessIndex(); + setErrorIndex(); + } + + + } else { + // Otherwise, let's tack them on to the end of the argument list + // which is 90% of cases + args.push(resolve); + args.push(reject); + } + return args; +} + +function callCordovaPlugin(pluginObj: any, methodName: string, args: any[], opts: any = {}, resolve?: Function, reject?: Function) { + // Try to figure out where the success/error callbacks need to be bound + // to our promise resolve/reject handlers. + args = setIndex(args, opts, resolve, reject); + + let pluginInstance = getPlugin(pluginObj.pluginRef); + + if (!pluginInstance || pluginInstance[methodName] === 'undefined') { + // Do this check in here in the case that the Web API for this plugin is available (for example, Geolocation). + if (!window.cordova) { + cordovaWarn(pluginObj.pluginName, methodName); + return { + error: 'cordova_not_available' + }; + } + + pluginWarn(pluginObj, methodName); + return { + error: 'plugin_not_installed' + }; + } + + return pluginInstance[methodName].apply(pluginInstance, args); +} + +/** + * @private + */ +export function getPromise(cb) { + + const tryNativePromise = () => { + if (window.Promise) { + return new Promise((resolve, reject) => { + cb(resolve, reject); + }); + } else { + console.error('No Promise support or polyfill found. To enable Ionic Native support, please add the es6-promise polyfill before this script, or run with a library like Angular 1/2 or on a recent browser.'); + } + }; + + if (window.angular) { + let injector = window.angular.element(document.querySelector('[ng-app]') || document.body).injector(); + if (injector) { + let $q = injector.get('$q'); + return $q((resolve, reject) => { + cb(resolve, reject); + }); + } else { + console.warn('Angular 1 was detected but $q couldn\'t be retrieved. This is usually when the app is not bootstrapped on the html or body tag. Falling back to native promises which won\'t trigger an automatic digest when promises resolve.'); + return tryNativePromise(); + } + } else { + return tryNativePromise(); + } +} + +function wrapPromise(pluginObj: any, methodName: string, args: any[], opts: any = {}) { + let pluginResult, rej; + const p = getPromise((resolve, reject) => { + pluginResult = callCordovaPlugin(pluginObj, methodName, args, opts, resolve, reject); + rej = reject; + }); + // Angular throws an error on unhandled rejection, but in this case we have already printed + // a warning that Cordova is undefined or the plugin is uninstalled, so there is no reason + // to error + if (pluginResult && pluginResult.error) { + p.catch(() => { }); + rej(pluginResult.error); + } + return p; +} + +function wrapOtherPromise(pluginObj: any, methodName: string, args: any[], opts: any= {}) { + return getPromise((resolve, reject) => { + let pluginResult = callCordovaPlugin(pluginObj, methodName, args, opts); + if (pluginResult && pluginResult.error) { + reject(pluginResult.error); + } + pluginResult.then(resolve).catch(reject); + }); +} + +function wrapObservable(pluginObj: any, methodName: string, args: any[], opts: any = {}) { + return new Observable(observer => { + let pluginResult = callCordovaPlugin(pluginObj, methodName, args, opts, observer.next.bind(observer), observer.error.bind(observer)); + if (pluginResult && pluginResult.error) { + observer.error(pluginResult.error); + } + return () => { + try { + if (opts.clearFunction) { + if (opts.clearWithArgs) { + return callCordovaPlugin(pluginObj, opts.clearFunction, args, opts, observer.next.bind(observer), observer.error.bind(observer)); + } + return get(window, pluginObj.pluginRef)[opts.clearFunction].call(pluginObj, pluginResult); + } + } catch (e) { + console.warn('Unable to clear the previous observable watch for', pluginObj.pluginName, methodName); + console.error(e); + } + }; + }); +} + +function callInstance(pluginObj: any, methodName: string, args: any[], opts: any = {}, resolve?: Function, reject?: Function) { + args = setIndex(args, opts, resolve, reject); + return pluginObj._objectInstance[methodName].apply(pluginObj._objectInstance, args); +} + +function wrapInstance(pluginObj: any, methodName: string, opts: any = {}) { + return (...args) => { + if (opts.sync) { + // Sync doesn't wrap the plugin with a promise or observable, it returns the result as-is + return callInstance(pluginObj, methodName, args, opts); + } else if (opts.observable) { + return new Observable(observer => { + let pluginResult = callInstance(pluginObj, methodName, args, opts, observer.next.bind(observer), observer.error.bind(observer)); + return () => { + try { + if (opts.clearWithArgs) { + return pluginObj._objectInstance[opts.clearFunction].apply(pluginObj._objectInstance, args); + } + return pluginObj._objectInstance[opts.clearFunction].call(pluginObj, pluginResult); + } catch (e) { + console.warn('Unable to clear the previous observable watch for', pluginObj.pluginName, methodName); + console.error(e); + } + }; + }); + } else if (opts.otherPromise) { + return getPromise((resolve, reject) => { + let result = callInstance(pluginObj, methodName, args, opts, resolve, reject); + result.then(resolve, reject); + }); + } else { + return getPromise((resolve, reject) => { + callInstance(pluginObj, methodName, args, opts, resolve, reject); + }); + } + }; +} + +/** + * Wrap the event with an observable + * @param event even name + * @param element The element to attach the event listener to + * @returns {Observable} + */ +function wrapEventObservable(event: string, element: any = window): Observable { + return Observable.fromEvent(element, event); +} + +/** + * Certain plugins expect the user to override methods in the plugin. For example, + * window.cordova.plugins.backgroundMode.onactivate = function() { ... }. + * + * Unfortunately, this is brittle and would be better wrapped as an Observable. overrideFunction + * does just this. + */ +function overrideFunction(pluginObj: any, methodName: string, args: any[], opts: any = {}): Observable { + return new Observable(observer => { + + let pluginInstance = getPlugin(pluginObj.pluginRef); + + if (!pluginInstance) { + // Do this check in here in the case that the Web API for this plugin is available (for example, Geolocation). + if (!window.cordova) { + cordovaWarn(pluginObj.pluginName, methodName); + observer.error({ + error: 'cordova_not_available' + }); + } + + pluginWarn(pluginObj, methodName); + observer.error({ + error: 'plugin_not_installed' + }); + return; + } + + let method = pluginInstance[methodName]; + if (!method) { + observer.error({ + error: 'no_such_method' + }); + observer.complete(); + return; + } + pluginInstance[methodName] = observer.next.bind(observer); + }); +} + + +/** + * @private + * @param pluginObj + * @param methodName + * @param opts + * @returns {function(...[any]): (undefined|*|Observable|*|*)} + */ +export const wrap = function(pluginObj: any, methodName: string, opts: CordovaOptions = {}) { + return (...args) => { + if (opts.sync) { + // Sync doesn't wrap the plugin with a promise or observable, it returns the result as-is + return callCordovaPlugin(pluginObj, methodName, args, opts); + } else if (opts.observable) { + return wrapObservable(pluginObj, methodName, args, opts); + } else if (opts.eventObservable && opts.event) { + return wrapEventObservable(opts.event, opts.element); + } else if (opts.otherPromise) { + return wrapOtherPromise(pluginObj, methodName, args, opts); + } else { + return wrapPromise(pluginObj, methodName, args, opts); + } + }; +}; + + +/** + * @private + * + * Class decorator specifying Plugin metadata. Required for all plugins. + * + * @usage + * ```typescript + * @Plugin({ + * pluginName: 'MyPlugin', + * plugin: 'cordova-plugin-myplugin', + * pluginRef: 'window.myplugin' + * }) + * export class MyPlugin { + * + * // Plugin wrappers, properties, and functions go here ... + * + * } + * ``` + */ +export function Plugin(config: PluginConfig) { + return function(cls) { + + // Add these fields to the class + for (let k in config) { + cls[k] = config[k]; + } + + cls['installed'] = function(printWarning?: boolean) { + return !!getPlugin(config.pluginRef); + }; + + cls['getPlugin'] = function() { + return getPlugin(config.pluginRef); + }; + + cls['checkInstall'] = function() { + let pluginInstance = getPlugin(config.pluginRef); + + if (!pluginInstance) { + pluginWarn(cls); + return false; + } + return true; + }; + + return cls; + }; +} + +/** + * @private + * + * Wrap a stub function in a call to a Cordova plugin, checking if both Cordova + * and the required plugin are installed. + */ +export function Cordova(opts: CordovaOptions = {}) { + return (target: Object, methodName: string, descriptor: TypedPropertyDescriptor) => { + return { + value: function(...args: any[]) { + return wrap(this, methodName, opts).apply(this, args); + } + }; + }; +} + +/** + * @private + * + * Wrap an instance method + */ +export function CordovaInstance(opts: any = {}) { + return (target: Object, methodName: string) => { + return { + value: function(...args: any[]) { + return wrapInstance(this, methodName, opts).apply(this, args); + } + }; + }; +} + +/** + * @private + * + * + * Before calling the original method, ensure Cordova and the plugin are installed. + */ +export function CordovaProperty(target: any, key: string) { + const exists = () => { + let pluginInstance = getPlugin(target.pluginRef); + if (!pluginInstance || typeof pluginInstance[key] === 'undefined') { + pluginWarn(target, key); + return false; + } + return true; + }; + + Object.defineProperty(target, key, { + get: () => { + if (exists()) { + return getPlugin(target.pluginRef)[key]; + } else { + return null; + } + }, + set: (value) => { + if (exists()) { + getPlugin(target.pluginRef)[key] = value; + } + } + }); +} + +/** + * @private + * @param target + * @param key + * @constructor + */ +export function InstanceProperty(target: any, key: string) { + Object.defineProperty(target, key, { + get: function(){ + return this._objectInstance[key]; + }, + set: function(value){ + this._objectInstance[key] = value; + } + }); +} + +/** + * @private + * + * Wrap a stub function in a call to a Cordova plugin, checking if both Cordova + * and the required plugin are installed. + */ +export function CordovaFunctionOverride(opts: any = {}) { + return (target: Object, methodName: string, descriptor: TypedPropertyDescriptor) => { + return { + value: function(...args: any[]) { + return overrideFunction(this, methodName, opts); + } + }; + }; +}