From 95475cceb4cbd5be2cc7e18f2cf3045eb6c6f7fd Mon Sep 17 00:00:00 2001 From: Dan Imhoff Date: Thu, 23 Jul 2020 11:19:49 -0700 Subject: [PATCH] feat(core): add `registerPlugin` for importing from plugin packages (#3305) --- cli/src/tasks/new-plugin.ts | 10 ++- core/src/index.ts | 3 +- core/src/plugins.ts | 133 ++++++++++++++++++++++++++++ core/src/web-plugins.ts | 47 ++++++++-- core/src/web/accessibility.ts | 5 +- core/src/web/app.ts | 5 +- core/src/web/browser.ts | 5 +- core/src/web/camera.ts | 5 +- core/src/web/clipboard.ts | 5 +- core/src/web/device.ts | 5 +- core/src/web/filesystem.ts | 5 +- core/src/web/geolocation.ts | 5 +- core/src/web/index.ts | 100 ++------------------- core/src/web/local-notifications.ts | 5 +- core/src/web/modals.ts | 5 +- core/src/web/network.ts | 5 +- core/src/web/share.ts | 5 +- core/src/web/splash-screen.ts | 5 +- core/src/web/storage.ts | 5 +- core/src/web/toast.ts | 5 +- plugin-template/src/definitions.ts | 4 +- plugin-template/src/index.ts | 23 ++++- plugin-template/src/web.ts | 12 +-- 23 files changed, 229 insertions(+), 178 deletions(-) create mode 100644 core/src/plugins.ts diff --git a/cli/src/tasks/new-plugin.ts b/cli/src/tasks/new-plugin.ts index 1a9cf48c8..1676c8dad 100644 --- a/cli/src/tasks/new-plugin.ts +++ b/cli/src/tasks/new-plugin.ts @@ -158,6 +158,10 @@ async function createTSPlugin( ) { const newPluginPath = join(pluginPath, 'src'); + const originalIndex = await readFileAsync( + join(newPluginPath, 'index.ts'), + 'utf8', + ); const originalDefinitions = await readFileAsync( join(newPluginPath, 'definitions.ts'), 'utf8', @@ -166,9 +170,11 @@ async function createTSPlugin( join(newPluginPath, 'web.ts'), 'utf8', ); - let definitions = originalDefinitions.replace(/Echo/g, className); + const index = originalIndex.replace(/MyPlugin/g, className); + const definitions = originalDefinitions.replace(/MyPlugin/g, className); const web = originalWeb.replace(/MyPlugin/g, className); + await writeFileAsync(join(newPluginPath, `index.ts`), index, 'utf8'); await writeFileAsync( join(newPluginPath, `definitions.ts`), definitions, @@ -321,7 +327,7 @@ function generatePackageJSON(answers: NewPluginAnswers, cliVersion: string) { rimraf: '^3.0.0', rollup: '^2.21.0', swiftlint: '^1.0.1', - typescript: '~3.8.3', + typescript: '~3.9.7', }, peerDependencies: { '@capacitor/core': `^${cliVersion}`, diff --git a/core/src/index.ts b/core/src/index.ts index 02c2fc725..9569a4c99 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -8,5 +8,6 @@ export { export * from './core-plugin-definitions'; export * from './global'; +export * from './plugins'; export * from './web-plugins'; -export * from './web/index'; +export * from './web'; diff --git a/core/src/plugins.ts b/core/src/plugins.ts new file mode 100644 index 000000000..aef8d3443 --- /dev/null +++ b/core/src/plugins.ts @@ -0,0 +1,133 @@ +import { Capacitor, Plugins } from './global'; +import { WebPlugin } from './web'; + +const PLUGIN_REGISTRY = new (class { + protected readonly plugins: { + [plugin: string]: RegisteredPlugin; + } = {}; + + get(name: string): RegisteredPlugin | undefined { + return this.plugins[name]; + } + + getAll(): RegisteredPlugin[] { + return Object.values(this.plugins); + } + + has(name: string): boolean { + return !!this.get(name); + } + + register(plugin: RegisteredPlugin): void { + this.plugins[plugin.name] = plugin; + } +})(); + +/** + * A map of plugin implementations. + * + * Each key should be the lowercased platform name as recognized by Capacitor, + * e.g. 'android', 'ios', and 'web'. Each value must be an instance of a plugin + * implementation for the respective platform. + */ +export type PluginImplementations = { + [platform: string]: T; +}; + +/** + * Represents a plugin registered with Capacitor. + */ +export class RegisteredPlugin { + constructor( + readonly name: string, + readonly implementations: Readonly>, + ) {} + + /** + * Return the appropriate implementation of this plugin. + * + * Supply a platform to return the implementation for it, otherwise this + * method will return the implementation for the current platform as detected + * by Capacitor. + * + * @param platform Optionally return the implementation of the given + * platform. + */ + getImplementation(platform?: string): T | undefined { + return this.implementations[platform ? platform : Capacitor.platform]; + } +} + +/** + * Register plugin implementations with Capacitor. + * + * This function will create and register an instance that contains the + * implementations of the plugin. + * + * Each plugin has multiple implementations, one per platform. Each + * implementation must adhere to a common interface to ensure client code + * behaves consistently across each platform. + * + * @param name The unique CamelCase name of this plugin. + * @param implementations The map of plugin implementations. + */ +export const registerPlugin = ( + name: string, + implementations: Readonly>, +): RegisteredPlugin => { + const plugin = new RegisteredPlugin(name, implementations); + PLUGIN_REGISTRY.register(plugin); + + return plugin; +}; + +/** + * TODO + * + * @deprecated Don't use this. + */ +export const registerWebPlugin = (plugin: WebPlugin) => { + console.warn( + `Capacitor plugin ${plugin.config.name} is using deprecated method 'registerWebPlugin'`, + ); // TODO: add link to upgrade guide + + if (!PLUGIN_REGISTRY.has(plugin.config.name)) { + const { name, platforms = ['web'] } = plugin.config; + const implementations: PluginImplementations = {}; + + PLUGIN_REGISTRY.register( + new RegisteredPlugin( + name, + platforms.reduce((acc, value) => { + acc[value] = plugin; + return acc; + }, implementations), + ), + ); + + mergeWebPlugin(plugin); + } +}; + +const shouldMergeWebPlugin = (plugin: WebPlugin) => { + return ( + plugin.config.platforms && + plugin.config.platforms.indexOf(Capacitor.platform) >= 0 + ); +}; + +/** + * TODO + * + * @deprecated Don't use this. + */ +export const mergeWebPlugin = (plugin: WebPlugin) => { + if ( + Plugins.hasOwnProperty(plugin.config.name) && + !shouldMergeWebPlugin(plugin) + ) { + return; + } + + Plugins[plugin.config.name] = plugin; +}; diff --git a/core/src/web-plugins.ts b/core/src/web-plugins.ts index 34d7e81d3..e8ee96b99 100644 --- a/core/src/web-plugins.ts +++ b/core/src/web-plugins.ts @@ -1,26 +1,55 @@ -import { Plugins } from './global'; -import { mergeWebPlugins, mergeWebPlugin, WebPlugin } from './web/index'; +import { mergeWebPlugin } from './plugins'; + +import { Accessibility } from './web/accessibility'; +import { App } from './web/app'; +import { Browser } from './web/browser'; +import { Camera } from './web/camera'; +import { Clipboard } from './web/clipboard'; +import { Device } from './web/device'; +import { Filesystem } from './web/filesystem'; +import { Geolocation } from './web/geolocation'; +import { LocalNotifications } from './web/local-notifications'; +import { Modals } from './web/modals'; +import { Motion } from './web/motion'; +import { Network } from './web/network'; +import { Permissions } from './web/permissions'; +import { Share } from './web/share'; +import { SplashScreen } from './web/splash-screen'; +import { Storage } from './web/storage'; +import { Toast } from './web/toast'; export * from './web/accessibility'; export * from './web/app'; export * from './web/browser'; export * from './web/camera'; export * from './web/clipboard'; +export * from './web/device'; export * from './web/filesystem'; export * from './web/geolocation'; -export * from './web/device'; export * from './web/local-notifications'; -export * from './web/share'; export * from './web/modals'; export * from './web/motion'; export * from './web/network'; export * from './web/permissions'; +export * from './web/share'; export * from './web/splash-screen'; export * from './web/storage'; export * from './web/toast'; -mergeWebPlugins(Plugins); - -export const registerWebPlugin = (plugin: WebPlugin) => { - mergeWebPlugin(Plugins, plugin); -}; +mergeWebPlugin(Accessibility); +mergeWebPlugin(App); +mergeWebPlugin(Browser); +mergeWebPlugin(Camera); +mergeWebPlugin(Clipboard); +mergeWebPlugin(Device); +mergeWebPlugin(Filesystem); +mergeWebPlugin(Geolocation); +mergeWebPlugin(LocalNotifications); +mergeWebPlugin(Modals); +mergeWebPlugin(Motion); +mergeWebPlugin(Network); +mergeWebPlugin(Permissions); +mergeWebPlugin(Share); +mergeWebPlugin(SplashScreen); +mergeWebPlugin(Storage); +mergeWebPlugin(Toast); diff --git a/core/src/web/accessibility.ts b/core/src/web/accessibility.ts index 3c1d1b48e..c9186831a 100644 --- a/core/src/web/accessibility.ts +++ b/core/src/web/accessibility.ts @@ -9,10 +9,7 @@ import { export class AccessibilityPluginWeb extends WebPlugin implements AccessibilityPlugin { constructor() { - super({ - name: 'Accessibility', - platforms: ['web'], - }); + super({ name: 'Accessibility' }); } isScreenReaderEnabled(): Promise { diff --git a/core/src/web/app.ts b/core/src/web/app.ts index ddd04fe06..110e1ff12 100644 --- a/core/src/web/app.ts +++ b/core/src/web/app.ts @@ -4,10 +4,7 @@ import { AppPlugin, AppLaunchUrl, AppState } from '../core-plugin-definitions'; export class AppPluginWeb extends WebPlugin implements AppPlugin { constructor() { - super({ - name: 'App', - platforms: ['web'], - }); + super({ name: 'App' }); if (typeof document !== 'undefined') { document.addEventListener( diff --git a/core/src/web/browser.ts b/core/src/web/browser.ts index 535bec140..0b7c590a1 100644 --- a/core/src/web/browser.ts +++ b/core/src/web/browser.ts @@ -10,10 +10,7 @@ export class BrowserPluginWeb extends WebPlugin implements BrowserPlugin { _lastWindow: Window; constructor() { - super({ - name: 'Browser', - platforms: ['web'], - }); + super({ name: 'Browser' }); } async open(options: BrowserOpenOptions): Promise { diff --git a/core/src/web/camera.ts b/core/src/web/camera.ts index 0d29e2e68..cf9077f02 100644 --- a/core/src/web/camera.ts +++ b/core/src/web/camera.ts @@ -11,10 +11,7 @@ import { export class CameraPluginWeb extends WebPlugin implements CameraPlugin { constructor() { - super({ - name: 'Camera', - platforms: ['web'], - }); + super({ name: 'Camera' }); } async getPhoto(options: CameraOptions): Promise { diff --git a/core/src/web/clipboard.ts b/core/src/web/clipboard.ts index f81cdb17f..fb5ddd0e7 100644 --- a/core/src/web/clipboard.ts +++ b/core/src/web/clipboard.ts @@ -11,10 +11,7 @@ declare var ClipboardItem: any; export class ClipboardPluginWeb extends WebPlugin implements ClipboardPlugin { constructor() { - super({ - name: 'Clipboard', - platforms: ['web'], - }); + super({ name: 'Clipboard' }); } async write(options: ClipboardWrite): Promise { diff --git a/core/src/web/device.ts b/core/src/web/device.ts index 3f91f216a..4d82aa9bc 100644 --- a/core/src/web/device.ts +++ b/core/src/web/device.ts @@ -13,10 +13,7 @@ declare var navigator: any; export class DevicePluginWeb extends WebPlugin implements DevicePlugin { constructor() { - super({ - name: 'Device', - platforms: ['web'], - }); + super({ name: 'Device' }); } async getInfo(): Promise { diff --git a/core/src/web/filesystem.ts b/core/src/web/filesystem.ts index ef5a09ce9..e4f43bd42 100644 --- a/core/src/web/filesystem.ts +++ b/core/src/web/filesystem.ts @@ -37,10 +37,7 @@ export class FilesystemPluginWeb extends WebPlugin implements FilesystemPlugin { static _debug: boolean = true; constructor() { - super({ - name: 'Filesystem', - platforms: ['web'], - }); + super({ name: 'Filesystem' }); } async initDb(): Promise { diff --git a/core/src/web/geolocation.ts b/core/src/web/geolocation.ts index d08487a74..4e8406536 100644 --- a/core/src/web/geolocation.ts +++ b/core/src/web/geolocation.ts @@ -14,10 +14,7 @@ import { extend } from '../util'; export class GeolocationPluginWeb extends WebPlugin implements GeolocationPlugin { constructor() { - super({ - name: 'Geolocation', - platforms: ['web'], - }); + super({ name: 'Geolocation' }); } getCurrentPosition( diff --git a/core/src/web/index.ts b/core/src/web/index.ts index 75af727bb..3949d4df3 100644 --- a/core/src/web/index.ts +++ b/core/src/web/index.ts @@ -1,46 +1,5 @@ -import { - Capacitor, - PluginListenerHandle, - PermissionsRequestResult, -} from '../definitions'; - -declare var Capacitor: Capacitor; - -export class WebPluginRegistry { - plugins: { [name: string]: WebPlugin } = {}; - loadedPlugins: { [name: string]: WebPlugin } = {}; - - constructor() {} - - addPlugin(plugin: WebPlugin) { - this.plugins[plugin.config.name] = plugin; - } - - getPlugin(name: string) { - return this.plugins[name]; - } - - loadPlugin(name: string) { - let plugin = this.getPlugin(name); - if (!plugin) { - console.error(`Unable to load web plugin ${name}, no such plugin found.`); - return; - } - - plugin.load(); - } - - getPlugins() { - let p = []; - for (let name in this.plugins) { - p.push(this.plugins[name]); - } - return p; - } -} - -let WebPlugins = new WebPluginRegistry(); -export { WebPlugins }; +import { PluginListenerHandle, PermissionsRequestResult } from '../definitions'; +import { Capacitor } from '../global'; export type ListenerCallback = (err: any, ...args: any[]) => void; @@ -55,12 +14,14 @@ export interface WebPluginConfig { /** * The name of the plugin */ - name: string; + readonly name: string; + /** - * The platforms this web plugin should run on. Leave null - * for this plugin to always run. + * TODO + * + * @deprecated Don't use this. */ - platforms?: string[]; + readonly platforms?: string[]; } export class WebPlugin { @@ -69,16 +30,7 @@ export class WebPlugin { listeners: { [eventName: string]: ListenerCallback[] } = {}; windowListeners: { [eventName: string]: WindowListenerHandle } = {}; - constructor( - public config: WebPluginConfig, - pluginRegistry?: WebPluginRegistry, - ) { - if (!pluginRegistry) { - WebPlugins.addPlugin(this); - } else { - pluginRegistry.addPlugin(this); - } - } + constructor(public config: WebPluginConfig) {} private addWindowListener(handle: WindowListenerHandle): void { window.addEventListener(handle.windowEventName, handle.handler); @@ -184,37 +136,3 @@ export class WebPlugin { this.loaded = true; } } - -const shouldMergeWebPlugin = (plugin: WebPlugin) => { - return ( - plugin.config.platforms && - plugin.config.platforms.indexOf(Capacitor.platform) >= 0 - ); -}; - -/** - * For all our known web plugins, merge them into the global plugins - * registry if they aren't already existing. If they don't exist, that - * means there's no existing native implementation for it. - * @param knownPlugins the Capacitor.Plugins global registry. - */ -export const mergeWebPlugins = (knownPlugins: any) => { - let plugins = WebPlugins.getPlugins(); - for (let plugin of plugins) { - mergeWebPlugin(knownPlugins, plugin); - } -}; - -export const mergeWebPlugin = (knownPlugins: any, plugin: WebPlugin) => { - // If we already have a plugin registered (meaning it was defined in the native layer), - // then we should only overwrite it if the corresponding web plugin activates on - // a certain platform. For example: Geolocation uses the WebPlugin on Android but not iOS - if ( - knownPlugins.hasOwnProperty(plugin.config.name) && - !shouldMergeWebPlugin(plugin) - ) { - return; - } - - knownPlugins[plugin.config.name] = plugin; -}; diff --git a/core/src/web/local-notifications.ts b/core/src/web/local-notifications.ts index 44152c893..420e9d64b 100644 --- a/core/src/web/local-notifications.ts +++ b/core/src/web/local-notifications.ts @@ -19,10 +19,7 @@ export class LocalNotificationsPluginWeb extends WebPlugin private pending: LocalNotification[] = []; constructor() { - super({ - name: 'LocalNotifications', - platforms: ['web'], - }); + super({ name: 'LocalNotifications' }); } createChannel(channel: NotificationChannel): Promise { diff --git a/core/src/web/modals.ts b/core/src/web/modals.ts index 0d4d02d2c..49a636258 100644 --- a/core/src/web/modals.ts +++ b/core/src/web/modals.ts @@ -13,10 +13,7 @@ import { export class ModalsPluginWeb extends WebPlugin implements ModalsPlugin { constructor() { - super({ - name: 'Modals', - platforms: ['web'], - }); + super({ name: 'Modals' }); } async alert(options: AlertOptions): Promise { diff --git a/core/src/web/network.ts b/core/src/web/network.ts index 8d41b522c..5c35645d3 100644 --- a/core/src/web/network.ts +++ b/core/src/web/network.ts @@ -11,10 +11,7 @@ export class NetworkPluginWeb extends WebPlugin implements NetworkPlugin { listenerFunction: any = null; constructor() { - super({ - name: 'Network', - platforms: ['web'], - }); + super({ name: 'Network' }); } getStatus(): Promise { diff --git a/core/src/web/share.ts b/core/src/web/share.ts index 8573b62e2..53c346663 100644 --- a/core/src/web/share.ts +++ b/core/src/web/share.ts @@ -6,10 +6,7 @@ declare var navigator: any; export class SharePluginWeb extends WebPlugin implements SharePlugin { constructor() { - super({ - name: 'Share', - platforms: ['web'], - }); + super({ name: 'Share' }); } share(options?: ShareOptions): Promise { diff --git a/core/src/web/splash-screen.ts b/core/src/web/splash-screen.ts index 236accc11..91506831b 100644 --- a/core/src/web/splash-screen.ts +++ b/core/src/web/splash-screen.ts @@ -9,10 +9,7 @@ import { export class SplashScreenPluginWeb extends WebPlugin implements SplashScreenPlugin { constructor() { - super({ - name: 'SplashScreen', - platforms: ['web'], - }); + super({ name: 'SplashScreen' }); } show( diff --git a/core/src/web/storage.ts b/core/src/web/storage.ts index 5289613a5..04c3360d1 100644 --- a/core/src/web/storage.ts +++ b/core/src/web/storage.ts @@ -6,10 +6,7 @@ export class StoragePluginWeb extends WebPlugin implements StoragePlugin { KEY_PREFIX = '_cap_'; constructor() { - super({ - name: 'Storage', - platforms: ['web'], - }); + super({ name: 'Storage' }); } get(options: { key: string }): Promise<{ value: string }> { diff --git a/core/src/web/toast.ts b/core/src/web/toast.ts index 4841825bc..b1162e8cd 100644 --- a/core/src/web/toast.ts +++ b/core/src/web/toast.ts @@ -3,10 +3,7 @@ import { ToastPlugin, ToastShowOptions } from '../core-plugin-definitions'; export class ToastPluginWeb extends WebPlugin implements ToastPlugin { constructor() { - super({ - name: 'Toast', - platforms: ['web'], - }); + super({ name: 'Toast' }); } async show(options: ToastShowOptions) { diff --git a/plugin-template/src/definitions.ts b/plugin-template/src/definitions.ts index 23d624e94..58a788fad 100644 --- a/plugin-template/src/definitions.ts +++ b/plugin-template/src/definitions.ts @@ -1,9 +1,9 @@ declare module '@capacitor/core' { interface PluginRegistry { - Echo: EchoPlugin; + MyPlugin: MyPluginPlugin; } } -export interface EchoPlugin { +export interface MyPluginPlugin { echo(options: { value: string }): Promise<{ value: string }>; } diff --git a/plugin-template/src/index.ts b/plugin-template/src/index.ts index 5c5939590..6b62ec0d1 100644 --- a/plugin-template/src/index.ts +++ b/plugin-template/src/index.ts @@ -1,2 +1,21 @@ -export * from './definitions'; -export * from './web'; +import { + Plugins, + PluginImplementations, + registerPlugin, +} from '@capacitor/core'; + +import { MyPluginPlugin } from './definitions'; +import { MyPluginWeb } from './web'; + +const implementations: PluginImplementations = { + android: Plugins.MyPlugin, + ios: Plugins.MyPlugin, + web: new MyPluginWeb(), +}; + +const MyPlugin = registerPlugin( + 'MyPlugin', + implementations, +).getImplementation(); + +export { MyPlugin }; diff --git a/plugin-template/src/web.ts b/plugin-template/src/web.ts index d3bccb308..26590f5f7 100644 --- a/plugin-template/src/web.ts +++ b/plugin-template/src/web.ts @@ -3,10 +3,7 @@ import { MyPluginPlugin } from './definitions'; export class MyPluginWeb extends WebPlugin implements MyPluginPlugin { constructor() { - super({ - name: 'MyPlugin', - platforms: ['web'], - }); + super({ name: 'MyPlugin' }); } async echo(options: { value: string }): Promise<{ value: string }> { @@ -14,10 +11,3 @@ export class MyPluginWeb extends WebPlugin implements MyPluginPlugin { return options; } } - -const MyPlugin = new MyPluginWeb(); - -export { MyPlugin }; - -import { registerWebPlugin } from '@capacitor/core'; -registerWebPlugin(MyPlugin);