diff --git a/android-template/app/src/main/java/com/getcapacitor/myapp/MainActivity.java b/android-template/app/src/main/java/com/getcapacitor/myapp/MainActivity.java index 222632f1c8..f0139e82d3 100644 --- a/android-template/app/src/main/java/com/getcapacitor/myapp/MainActivity.java +++ b/android-template/app/src/main/java/com/getcapacitor/myapp/MainActivity.java @@ -1,21 +1,5 @@ package com.getcapacitor.myapp; -import android.os.Bundle; - import com.getcapacitor.BridgeActivity; -import com.getcapacitor.Plugin; - -import java.util.ArrayList; - -public class MainActivity extends BridgeActivity { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // Initializes the Bridge - this.init(savedInstanceState, new ArrayList>() {{ - // Additional plugins you've installed go here - // Ex: add(TotallyAwesomePlugin.class); - }}); - } -} +public class MainActivity extends BridgeActivity {} diff --git a/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java b/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java index 41b2d97fb6..287cc731d8 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java +++ b/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java @@ -17,7 +17,13 @@ import org.apache.cordova.CordovaPreferences; import org.apache.cordova.PluginEntry; import org.apache.cordova.PluginManager; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONTokener; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; @@ -40,6 +46,11 @@ public class BridgeActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + try { + this.init(savedInstanceState, loadPluginList("plugin-imports.json")); + } catch (IOException | JSONException | ClassNotFoundException e) { + e.printStackTrace(); + } } protected void init(Bundle savedInstanceState, List> plugins) { @@ -57,6 +68,34 @@ protected void init(Bundle savedInstanceState, List> plu this.load(savedInstanceState); } + @SuppressWarnings("unchecked") + ArrayList> loadPluginList(String fileName) throws IOException, JSONException, ClassNotFoundException { + ArrayList> pluginList = new ArrayList<>(); + JSONArray plugins = loadJsonArray(fileName); + for (int iPlugin = 0; iPlugin < plugins.length(); ++iPlugin) { + String plugin = (String) plugins.get(iPlugin); + pluginList.add((Class) Class.forName(plugin)); + } + return pluginList; + } + + private JSONArray loadJsonArray(String fileName) throws IOException, JSONException { + JSONTokener tokener = new JSONTokener(loadJsonStringFromFile(fileName)); + return new JSONArray(tokener); + } + + private String loadJsonStringFromFile(String fileName) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(getAssets().open(fileName))); + // do reading, usually loop until end of file reading + StringBuilder b = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + //process line + b.append(line); + } + return b.toString(); + } + /** * Load the WebView and create the Bridge */ diff --git a/cli/package.json b/cli/package.json index bbdcfa9e23..52c1bb740f 100644 --- a/cli/package.json +++ b/cli/package.json @@ -52,6 +52,7 @@ "open": "^6.1.0", "ora": "^1.3.0", "plist": "^3.0.1", + "read-installed": "^4.0.3", "semver": "^5.4.1", "which": "^1.3.0", "xml2js": "^0.4.19" @@ -61,6 +62,7 @@ "@types/inquirer": "0.0.35", "@types/jest": "^23.3.9", "@types/mock-fs": "^3.6.30", + "@types/node": "12.12.21", "@types/open": "^6.1.0", "@types/ora": "^1.3.1", "@types/semver": "^5.4.0", diff --git a/cli/src/android/update.ts b/cli/src/android/update.ts index fa270377c8..f6f4f96e83 100644 --- a/cli/src/android/update.ts +++ b/cli/src/android/update.ts @@ -1,5 +1,5 @@ import { Config } from '../config'; -import { checkPlatformVersions, logFatal, resolveNode, runTask } from '../common'; +import { checkPlatformVersions, log, logFatal, readXML, resolveNode, runTask } from '../common'; import { getAndroidPlugins } from './common'; import { checkAndInstallDependencies, handleCordovaPluginsJS, writeCordovaAndroidManifest } from '../cordova'; import { convertToUnixPath, copySync, existsSync, readFileAsync, removeSync, writeFileAsync} from '../util/fs'; @@ -8,8 +8,25 @@ import { Plugin, PluginType, getAllElements, getFilePath, getPlatformElement, ge const platform = 'android'; -export async function updateAndroid(config: Config) { - let plugins = await getPluginsTask(config); +async function listAndroidClasses(plugins: Plugin[], config: Config): Promise { + return Promise.all( + plugins.map(async (p: Plugin) => { + const manifestPath = resolve(p.rootPath, p.android!.path, 'src', 'main', 'AndroidManifest.xml'); + const pluginManifest = await readXML(manifestPath); + return `${pluginManifest.manifest.$.package}.${p.name}`; + }) + ); +} + +async function writePluginImports(plugins: Plugin[], config: Config) { + const compatiblePlugins = plugins.filter((p: Plugin) => getPluginType(p, platform) !== PluginType.Incompatible); + const imports = await listAndroidClasses(compatiblePlugins, config); + const writePath = resolve(config.android.platformDir, 'plugin-imports.json') + await writeFileAsync(writePath, JSON.stringify(imports)) +} + +export async function updateAndroid(config: Config, allPlugins: Plugin[]) { + let plugins = await getPluginsTask(config, allPlugins); const capacitorPlugins = plugins.filter(p => getPluginType(p, platform) === PluginType.Core); @@ -17,7 +34,7 @@ export async function updateAndroid(config: Config) { while (needsPluginUpdate) { needsPluginUpdate = await checkAndInstallDependencies(config, plugins, platform); if (needsPluginUpdate) { - plugins = await getPluginsTask(config); + plugins = await getPluginsTask(config, allPlugins); } } @@ -29,10 +46,11 @@ export async function updateAndroid(config: Config) { if (cordovaPlugins.length > 0) { copyPluginsNativeFiles(config, cordovaPlugins); } - await handleCordovaPluginsJS(cordovaPlugins, config, platform); + await handleCordovaPluginsJS(allPlugins, cordovaPlugins, config, platform); await installGradlePlugins(config, capacitorPlugins, cordovaPlugins); await handleCordovaPluginsGradle(config, cordovaPlugins); await writeCordovaAndroidManifest(cordovaPlugins, config, platform); + await writePluginImports(plugins, config); const incompatibleCordovaPlugins = plugins .filter(p => getPluginType(p, platform) === PluginType.Incompatible); @@ -190,9 +208,8 @@ function removePluginsNativeFiles(config: Config) { copySync(config.android.assets.pluginsDir, pluginsRoot); } -async function getPluginsTask(config: Config) { +async function getPluginsTask(config: Config, allPlugins: Plugin[]) { return await runTask('Updating Android plugins', async () => { - const allPlugins = await getPlugins(config); const androidPlugins = getAndroidPlugins(allPlugins); return androidPlugins; }); diff --git a/cli/src/cordova.ts b/cli/src/cordova.ts index fb1b9e858e..ed0447aae1 100644 --- a/cli/src/cordova.ts +++ b/cli/src/cordova.ts @@ -204,9 +204,9 @@ function getWebDir(config: Config, platform: string): string { return ''; } -export async function handleCordovaPluginsJS(cordovaPlugins: Plugin[], config: Config, platform: string) { +export async function handleCordovaPluginsJS(allPlugins: Plugin[], cordovaPlugins: Plugin[], config: Config, platform: string) { if (!existsSync(getWebDir(config, platform))) { - await copy(config, platform); + await copy(config, allPlugins, platform); } if (cordovaPlugins.length > 0) { printPlugins(cordovaPlugins, platform, 'cordova'); @@ -219,8 +219,7 @@ export async function handleCordovaPluginsJS(cordovaPlugins: Plugin[], config: C await autoGenerateConfig(config, cordovaPlugins, platform); } -export async function getCordovaPlugins(config: Config, platform: string): Promise { - const allPlugins = await getPlugins(config); +export async function getCordovaPlugins(config: Config, allPlugins: Plugin[], platform: string): Promise { let plugins: Plugin[] = []; if (platform === config.ios.name) { plugins = getIOSPlugins(allPlugins); diff --git a/cli/src/ios/update.ts b/cli/src/ios/update.ts index 6ec1176a47..41bbb192d0 100644 --- a/cli/src/ios/update.ts +++ b/cli/src/ios/update.ts @@ -11,9 +11,9 @@ import { checkAndInstallDependencies, handleCordovaPluginsJS, logCordovaManualSt export const updateIOSChecks: CheckFunction[] = [checkCocoaPods, checkIOSProject]; const platform = 'ios'; -export async function updateIOS(config: Config, deployment: boolean) { +export async function updateIOS(config: Config, allPlugins: Plugin[], deployment: boolean) { - let plugins = await getPluginsTask(config); + let plugins = await getPluginsTask(config, allPlugins); const capacitorPlugins = plugins.filter(p => getPluginType(p, platform) === PluginType.Core); @@ -23,7 +23,7 @@ export async function updateIOS(config: Config, deployment: boolean) { while (needsPluginUpdate) { needsPluginUpdate = await checkAndInstallDependencies(config, plugins, platform); if (needsPluginUpdate) { - plugins = await getPluginsTask(config); + plugins = await getPluginsTask(config, allPlugins); } } @@ -33,7 +33,7 @@ export async function updateIOS(config: Config, deployment: boolean) { if (cordovaPlugins.length > 0) { copyPluginsNativeFiles(config, cordovaPlugins); } - await handleCordovaPluginsJS(cordovaPlugins, config, platform); + await handleCordovaPluginsJS(allPlugins, cordovaPlugins, config, platform); await generateCordovaPodspecs(cordovaPlugins, config); await installCocoaPodsPlugins(config, plugins, deployment); await logCordovaManualSteps(cordovaPlugins, config, platform); @@ -348,9 +348,8 @@ function removeNoSystem(library: string, sourceFrameworks: Array) { return libraries.length === 0; } -async function getPluginsTask(config: Config) { +async function getPluginsTask(config: Config, allPlugins: Plugin[]) { return await runTask('Updating iOS plugins', async () => { - const allPlugins = await getPlugins(config); const iosPlugins = getIOSPlugins(allPlugins); return iosPlugins; }); diff --git a/cli/src/plugin.ts b/cli/src/plugin.ts index 1368d70a0a..8c5a85b00c 100644 --- a/cli/src/plugin.ts +++ b/cli/src/plugin.ts @@ -1,7 +1,9 @@ +import readInstalled = require('read-installed'); +import semver = require('semver'); import { Config } from './config'; import { join } from 'path'; -import { log, logFatal, readJSON, readXML, resolveNode } from './common'; - +import { log, logFatal, logWarn, readJSON, readXML, resolveNode, runTask } from './common'; +import { existsAsync } from './util/fs'; export const enum PluginType { Core, @@ -37,55 +39,87 @@ export interface Plugin { }; } -export async function getPlugins(config: Config): Promise { - const deps = getDependencies(config); - const plugins = await Promise.all(deps.map(async p => resolvePlugin(config, p))); - return plugins.filter(p => !!p) as Plugin[]; +function flatten(pkg: any) { + let seen: any[] = []; + const _flatten = (pkg: any): any[] => { + if (pkg.constructor !== Object || seen.indexOf(pkg) >= 0) return []; + seen.push(pkg); + if (!Object.keys(pkg.dependencies)) return [pkg]; + return [ pkg, ...([] as any[]).concat(...Object.values(pkg.dependencies).map(_flatten)) ]; + }; + return _flatten(pkg); } -export async function resolvePlugin(config: Config, name: string): Promise { - try { - const rootPath = resolveNode(config, name); - if (!rootPath) { - logFatal(`Unable to find node_modules/${name}. Are you sure ${name} is installed?`); - return null; + +function getInstalled(config: Config): Promise { + return new Promise((resolve, reject) => { + readInstalled(config.app.rootDir, { dev: true }, + (err: Error, tree) => err ? reject(err) : resolve(tree)); + }); +} + +export async function getPlugins(config: Config): Promise { + return await runTask('Finding plugins', async () => { + const rootPkg = await getInstalled(config); + const nullablePlugins = await Promise.all(flatten(rootPkg).map(resolvePlugin)); + const plugins: Plugin[] = nullablePlugins.filter(p => p !== null) as Plugin[]; + + const pluginMap = plugins.reduce((o, p) => { + (o[p.id] = o[p.id] || []).push(p); + return o; + }, {} as { [key: string]: Plugin[] }); + + const returnedPlugins = []; + for (let id in pluginMap) { + const variations = pluginMap[id]; + if (variations.length > 1) { + variations.sort((a, b) => semver.rcompare(a.version, b.version)); + const usedVersion = variations[0].version; + log(); + logWarn(`Found multiple versions of plugin ${id} using ${usedVersion}.`); + variations.slice(1).forEach((plugin) => { + const otherVersion = plugin.version; + const diff = semver.diff(usedVersion, otherVersion); + if ( diff !== null ) { // if not equal + logWarn(` ${usedVersion} is a ${diff} change ahead of ${otherVersion}, which was found but unused.`); + } + }); + } + returnedPlugins.push(variations[0]); } + return returnedPlugins; + }); +} - const packagePath = join(rootPath, 'package.json'); - const meta = await readJSON(packagePath); - if (!meta) { - return null; +export async function resolvePlugin(pkg: any): Promise { + try { + if (pkg.capacitor) { + return { + id: pkg.name, + name: fixName(pkg.name), + version: pkg.version, + rootPath: pkg.path, + repository: pkg.repository, + manifest: pkg.capacitor + }; } - if (meta.capacitor) { + const xmlPath = join(pkg.path, 'plugin.xml'); + if (await existsAsync(xmlPath)) return null; + const xml = await readXML(xmlPath); + if (xml) { return { - id: name, - name: fixName(name), - version: meta.version, - rootPath: rootPath, - repository: meta.repository, - manifest: meta.capacitor + id: pkg.name, + name: fixName(pkg.name), + version: pkg.version, + rootPath: pkg.path, + repository: pkg.repository, + xml: xml.plugin }; } - const pluginXMLPath = join(rootPath, 'plugin.xml'); - const xmlMeta = await readXML(pluginXMLPath); - return { - id: name, - name: fixName(name), - version: meta.version, - rootPath: rootPath, - repository: meta.repository, - xml: xmlMeta.plugin - }; - } catch (e) { } + } catch (_) {} return null; } -export function getDependencies(config: Config): string[] { - const dependencies = config.app.package.dependencies ? config.app.package.dependencies : []; - const devDependencies = config.app.package.devDependencies ? config.app.package.devDependencies : []; - return Object.keys(dependencies).concat(Object.keys(devDependencies)); -} - export function fixName(name: string): string { name = name .replace(/\//g, '_') diff --git a/cli/src/read-installed.d.ts b/cli/src/read-installed.d.ts new file mode 100644 index 0000000000..02473a4518 --- /dev/null +++ b/cli/src/read-installed.d.ts @@ -0,0 +1,18 @@ +interface Opt { + dev?: boolean, + log?: () => void, + depth?: number, +} + +interface PkgNode { + path: string, + parent: PkgNode, + dependencies: PkgNode[], +} + +type Func = (err: Error, tree: PkgNode) => void; + +declare module 'read-installed' { + function readInstalled(folder: string, options: Opt, func: Func): void; + export = readInstalled; +} diff --git a/cli/src/tasks/add.ts b/cli/src/tasks/add.ts index 3564ffcaec..143e10ac66 100644 --- a/cli/src/tasks/add.ts +++ b/cli/src/tasks/add.ts @@ -7,6 +7,7 @@ import { editProjectSettingsAndroid } from '../android/common'; import { editProjectSettingsIOS } from '../ios/common'; import { check, checkAppConfig, checkPackage, checkWebDir, log, logError, logFatal, logInfo, runTask, writePrettyJSON } from '../common'; import { sync } from './sync'; +import { getPlugins } from '../plugin'; import chalk from 'chalk'; import { resolve } from 'path'; @@ -41,7 +42,8 @@ export async function addCommand(config: Config, selectedPlatformName: string) { await editPlatforms(config, platformName); if (shouldSync(config, platformName)) { - await sync(config, platformName, false); + const allPlugins = await getPlugins(config); + await sync(config, platformName, allPlugins, false); } if (platformName === config.ios.name || platformName === config.android.name) { diff --git a/cli/src/tasks/copy.ts b/cli/src/tasks/copy.ts index c50167f234..70c55f8290 100644 --- a/cli/src/tasks/copy.ts +++ b/cli/src/tasks/copy.ts @@ -7,6 +7,7 @@ import { copyElectron } from '../electron/copy'; import { basename, join, relative, resolve } from 'path'; import { copy as fsCopy, remove } from 'fs-extra'; import { getCordovaPlugins, handleCordovaPluginsJS, writeCordovaAndroidManifest } from '../cordova'; +import { Plugin, getPlugins } from '../plugin'; import chalk from 'chalk'; export async function copyCommand(config: Config, selectedPlatformName: string) { @@ -16,13 +17,14 @@ export async function copyCommand(config: Config, selectedPlatformName: string) return; } try { - await allSerial(platforms.map(platformName => () => copy(config, platformName))); + const allPlugins = await getPlugins(config); + await allSerial(platforms.map(platformName => () => copy(config, allPlugins, platformName))); } catch (e) { logError(e); } } -export async function copy(config: Config, platformName: string) { +export async function copy(config: Config, allPlugins: Plugin[], platformName: string) { await runTask(chalk`{green {bold copy}}`, async () => { const result = await checkWebDir(config); @@ -34,14 +36,14 @@ export async function copy(config: Config, platformName: string) { await copyWebDir(config, config.ios.webDirAbs); await copyNativeBridge(config, config.ios.webDirAbs); await copyCapacitorConfig(config, join(config.ios.platformDir, config.ios.nativeProjectName, config.ios.nativeProjectName)); - const cordovaPlugins = await getCordovaPlugins(config, platformName); - await handleCordovaPluginsJS(cordovaPlugins, config, platformName); + const cordovaPlugins = await getCordovaPlugins(config, allPlugins, platformName); + await handleCordovaPluginsJS(allPlugins, cordovaPlugins, config, platformName); } else if (platformName === config.android.name) { await copyWebDir(config, config.android.webDirAbs); await copyNativeBridge(config, config.android.webDirAbs); await copyCapacitorConfig(config, join(config.android.platformDir, 'app/src/main/assets')); - const cordovaPlugins = await getCordovaPlugins(config, platformName); - await handleCordovaPluginsJS(cordovaPlugins, config, platformName); + const cordovaPlugins = await getCordovaPlugins(config, allPlugins, platformName); + await handleCordovaPluginsJS(allPlugins, cordovaPlugins, config, platformName); await writeCordovaAndroidManifest(cordovaPlugins, config, platformName); } else if (platformName === config.web.name) { await copyWeb(config); diff --git a/cli/src/tasks/create.ts b/cli/src/tasks/create.ts index 7f559a156f..86c1ff4b57 100644 --- a/cli/src/tasks/create.ts +++ b/cli/src/tasks/create.ts @@ -73,7 +73,7 @@ export async function createCommand(config: Config, dir: string, name: string, i return installDeps(appDir, ['@capacitor/cli', '@capacitor/core'], config); }); // Copy web and capacitor to web assets - await copy(config, config.web.name); + await copy(config, [], config.web.name); // Say something nice printNextSteps(config, appDir); } catch (e) { diff --git a/cli/src/tasks/list.ts b/cli/src/tasks/list.ts index 566a31e4cc..42590d6b23 100644 --- a/cli/src/tasks/list.ts +++ b/cli/src/tasks/list.ts @@ -12,15 +12,14 @@ export async function listCommand(config: Config, selectedPlatformName: string) return; } try { - await allSerial(platforms.map(platformName => () => list(config, platformName))); + const allPlugins = await getPlugins(config); + await allSerial(platforms.map(platformName => () => list(config, platformName, allPlugins))); } catch (e) { logError(e); } } -export async function list(config: Config, platform: string) { - - const allPlugins = await getPlugins(config); +export async function list(config: Config, platform: string, allPlugins: Plugin[]) { let plugins: Plugin[] = []; if (platform === config.ios.name) { plugins = getIOSPlugins(allPlugins); diff --git a/cli/src/tasks/sync.ts b/cli/src/tasks/sync.ts index fb562b8a42..60e59be174 100644 --- a/cli/src/tasks/sync.ts +++ b/cli/src/tasks/sync.ts @@ -2,7 +2,7 @@ import { Config } from '../config'; import { copy } from './copy'; import { update, updateChecks } from './update'; import { check, checkPackage, checkWebDir, log, logError, logFatal, logInfo } from '../common'; - +import { Plugin, getPlugins } from '../plugin'; import { allSerial } from '../util/promise'; /** @@ -17,7 +17,8 @@ export async function syncCommand(config: Config, selectedPlatform: string, depl } try { await check(config, [checkPackage, checkWebDir, ...updateChecks(config, platforms)]); - await allSerial(platforms.map(platformName => () => sync(config, platformName, deployment))); + const allPlugins = await getPlugins(config); + await allSerial(platforms.map(platformName => () => sync(config, platformName, allPlugins, deployment))); const now = +new Date; const diff = (now - then) / 1000; log(`Sync finished in ${diff}s`); @@ -26,11 +27,11 @@ export async function syncCommand(config: Config, selectedPlatform: string, depl } } -export async function sync(config: Config, platformName: string, deployment: boolean) { +export async function sync(config: Config, platformName: string, allPlugins: Plugin[], deployment: boolean) { try { - await copy(config, platformName); + await copy(config, allPlugins, platformName); } catch (e) { logError(e); } - await update(config, platformName, deployment); + await update(config, allPlugins, platformName, deployment); } diff --git a/cli/src/tasks/update.ts b/cli/src/tasks/update.ts index 1b5b684a04..eb2bb5dbd3 100644 --- a/cli/src/tasks/update.ts +++ b/cli/src/tasks/update.ts @@ -3,6 +3,7 @@ import { updateAndroid } from '../android/update'; import { updateIOS, updateIOSChecks } from '../ios/update'; import { allSerial } from '../util/promise'; import { CheckFunction, check, checkPackage, log, logError, logFatal, logInfo, runTask } from '../common'; +import { Plugin, getPlugins } from '../plugin'; import chalk from 'chalk'; @@ -19,7 +20,8 @@ export async function updateCommand(config: Config, selectedPlatformName: string [checkPackage, ...updateChecks(config, platforms)] ); - await allSerial(platforms.map(platformName => async () => await update(config, platformName, deployment))); + const allPlugins = await getPlugins(config); + await allSerial(platforms.map(platformName => async () => await update(config, allPlugins, platformName, deployment))); const now = +new Date; const diff = (now - then) / 1000; log(`Update finished in ${diff}s`); @@ -46,13 +48,13 @@ export function updateChecks(config: Config, platforms: string[]): CheckFunction return checks; } -export async function update(config: Config, platformName: string, deployment: boolean) { +export async function update(config: Config, plugins: Plugin[], platformName: string, deployment: boolean) { try { await runTask(chalk`{green {bold update}} {bold ${platformName}}`, async () => { if (platformName === config.ios.name) { - await updateIOS(config, deployment); + await updateIOS(config, plugins, deployment); } else if (platformName === config.android.name) { - await updateAndroid(config); + await updateAndroid(config, plugins); } }); } catch (e) { diff --git a/cli/tsconfig.json b/cli/tsconfig.json index 83dcf7ff29..06067f291a 100644 --- a/cli/tsconfig.json +++ b/cli/tsconfig.json @@ -6,11 +6,10 @@ "module": "commonjs", "target": "es2017", "moduleResolution": "node", - "pretty": true, - //"noUnusedLocals": true, - //"noUnusedParameters": true + "pretty": true }, "files": [ - "src/index.ts" + "src/index.ts", + "src/read-installed.d.ts" ] }