diff --git a/README.md b/README.md index 3845bea..08c7639 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,49 @@ document.addEventListener('DOMContentLoaded', function () { }); ``` +The context object may take the following additional, optional parameters: + * pluginPath: String: The path to find plugins. Defaults to ApplicationRoot/plugins + * Substitutes: + ~ - User home dir per os.homedir() IF first character + : - Application Name per package.json IF last character + * Examples: + ~/.config/: - /home/user/.config/MyApp + ~: - /home/usr/MyApp + * makePluginPath: Boolean: Create the plugin path if it is not found. Defaults to false + * quiet: Boolean: Do not write to the console. Defaults to false. + * plugins: An Object of user provided plugins in the format { pluginName: pluginVerionsString } using semvar compatible strings OR LINK for the version. + +If you would prefer to do plugin discovery, you can load using the following: + +``` +var plugins = require('electron-plugins'); + +document.addEventListener('DOMContentLoaded', function () { + plugins.discover( '', false, function ( err, results ) { + var context = { document: document }; + if( !err ) + { + context.plugins = results; + } + plugins.load({context: context}, function (err, loaded) { + if(err) return console.error(err); + console.log('Plugins loaded successfully.'); + }); + }); +}); +``` + +## About your plugin: + +The default plugin folder location is pulled from the AppDirectory object for the application if not provided by the app. +Inside the plugins folder, your plugin should have directory matching its name. There should be a subdirectory for each version of the plugin installed, named for the version in semvar compatible format ( example: 0.0.1 ) or LINK for developmental code. + +Your plugin will need a package.json. If config.main is not set, it is assumed to be index.js. + Your plugin should export a constructor function, which is passed the context object upon instantiation. You can put whatever you want onto the context object. ``` function Plugin(context) { - var d = context.document - var ul = d.getElementById('plugins') - var li = d.createElement('li') - li.innerHTML = 'electron-updater-sample-plugin' - ul.appendChild(li) + alert("This plugin loaded!"); } module.exports = Plugin diff --git a/index.js b/index.js index aac2bae..85b47b3 100644 --- a/index.js +++ b/index.js @@ -1,100 +1,220 @@ -var path = require('path'), - fs = require('fs'), - async = require('async'), - AppDirectory = require('appdirectory') +const path = require("path"); +const fs = require("fs"); +const async = require("async"); +const AppDirectory = require("appdirectory"); +const compareVersions = require( "compare-versions"); + + +var beLoud = true; function getPlugins(plugins) { - var mapped = [] + var mapped = []; Object.getOwnPropertyNames(plugins).forEach(function (name) { mapped.push({ name: name, version: plugins[name] - }) - }) + }); + }); - return mapped + return mapped; } function getPluginPackage(plugin, callback) { - var context = this - var name = plugin.name - var version = plugin.version + var context = this; + var name = plugin.name; + var version = plugin.version; // Load the package.json in either the linked dev directory or from the downloaded plugin async.map( - ['link', version], + ["link", version], function (version, callback) { - var packagePath = path.join(context.pluginsDir, name, version, 'package.json') - fs.readFile(packagePath, {encoding:'utf8'}, function (err, result) { - if(err) return callback() + var packagePath = path.join(context.pluginsDir, name, version, "package.json"); + fs.readFile(packagePath, {encoding:"utf8"}, function (err, result) { + if(err) { return callback(); } callback(null, { name: name, version: version, config: JSON.parse(result) - }) - }) + }); + }); }, function (err, results) { // If neither file is found, or there was an unexpected error then fail - var result = results[0] || results[1] - if (err || !result) return callback(err || 'ENOENT') - callback(null, result) - }) + var result = results[0] || results[1]; + if (err || !result) { return callback(err || "ENOENT"); } + callback(null, result); + }); } function loadPlugin(context, results, callback) { - var modules = [] - var dependencies = [] + var modules = []; + var dependencies = []; try { - for(var i = 0, n = results.length; i < n; i++) { - var plugin = results[i] - var main = plugin.config.main - var name = plugin.name - var version = plugin.version - var depName = name.replace(/-/g, '.') - var file = path.resolve(path.join(context.pluginsDir, name, version), main) - var Plugin = require(file) - var mod = new Plugin(context.appContext) - modules.push(mod) - dependencies.push(depName) + for ( var i = 0, n = results.length; i < n; i++) { + var plugin = results[i]; + var main = plugin.config.hasOwnProperty("main") ? plugin.config.main : "index.js"; + var name = plugin.name; + var version = plugin.version; + var depName = name.replace(/-/g, "."); + var file = path.resolve(path.join(context.pluginsDir, name, version), main); + var Plugin = require(file); + var mod = new Plugin(context.appContext); + modules.push(mod); + dependencies.push(depName); } } catch (err) { - return callback(err) + return callback(err); } + callback(null, dependencies, modules); +} - callback(null, dependencies, modules) +/* FROM https://stackoverflow.com/questions/31645738/how-to-create-full-path-with-nodes-fs-mkdirsync */ + +function mkDirByPathSync(targetDir, {isRelativeToScript = false} = {}) { + const sep = path.sep; + const initDir = path.isAbsolute(targetDir) ? sep : ""; + const baseDir = isRelativeToScript ? __dirname : "."; + + targetDir.split(sep).reduce((parentDir, childDir) => { + const curDir = path.resolve(baseDir, parentDir, childDir); + try { + fs.mkdirSync(curDir); + if ( beLoud ) { console.log(`Directory ${curDir} created!`); } + } catch (err) { + if (err.code !== "EEXIST") { + throw err; + } + + if ( beLoud ) { console.log(`Directory ${curDir} already exists!`); } + } + + return curDir; + }, initDir); } -function load(appContext, callback) { - var appDir = path.dirname(process.mainModule.filename) - var packagePath = path.join(appDir, 'package.json') - fs.readFile(packagePath, {encoding: 'utf8'}, function (err, contents) { - if(err) return callback(err); - var config = JSON.parse(contents) - var dirs = new AppDirectory({ - appName: config.name, - appAuthor: config.publisher - }) - var appData = dirs.userData() - console.log('appData: ' + appData) - var currentPath = path.join(appData, '.current') - fs.readFile(currentPath, {encoding: 'utf8'}, function (err, contents) { - var plugins = (!err ? JSON.parse(contents) : config.plugins) || {} - var context = { - plugins: plugins, - pluginsDir: path.join(appData, 'plugins'), - appContext: appContext +function substitutePluginPath( passedPath, appDir, callback ) { + let pluginPath = ""; + + var packagePath = path.join(appDir, "package.json"); + + fs.readFile(packagePath, {encoding: "utf8"}, function (err, contents) { + if(err) return callback(err, null); + var config = JSON.parse(contents) + var dirs = new AppDirectory({ + appName: config.name, + appAuthor: config.publisher + }) + var appData = dirs.userData() + if ( beLoud ) { console.log("[electron-plugins] appData: " + appData); } + + pluginPath = passedPath ? passedPath : path.join(appData, "plugins"); + + // Check to see if this is a "relative path", ASSUME ~ is homedir for all platforms. + if( pluginPath.slice( 0, 1 ) === "~" ) + { + pluginPath = path.join( require( "os" ).homedir(), pluginPath.substr(1) ); + } + if( pluginPath.slice( -1 ) === ":" ) + { + pluginPath = path.join( pluginPath.slice( 0, -1 ), config.name ); + } + if ( beLoud ) { console.log( "[electron-plugins] Using provided plugin path. " + pluginPath ); } + + return callback( null, pluginPath ); + } ); +} + +function discover( pluginRelPath, appD, useDev, callback ) { + let foundPlugins = {}; + var appDir = appD ? appD : path.dirname(process.mainModule.filename); + substitutePluginPath( pluginRelPath, appDir, function ( err, pluginPath ) { + if( fs.existsSync( pluginPath ) ) { + dirContents = fs.readdirSync( pluginPath ); + for( var contLoop=0; contLoop < dirContents.length; contLoop++ ) + { + if( fs.statSync( path.join(pluginPath, dirContents[ contLoop ])).isDirectory() ) + { + const versions = fs.readdirSync( path.join(pluginPath, dirContents[ contLoop ] ) ); + var newestVersion = "0.0.0"; + for ( var verLoop=0; verLoop < versions.length; verLoop++ ) { + if( fs.statSync( path.join( pluginPath, dirContents[ contLoop ], versions[ verLoop ])).isDirectory() ) { + if( versions[ verLoop ] === "LINK" ) + { + if ( useDev ) + { + foundPlugins[ dirContents[ contLoop ] ] = "LINK"; + } + continue; + } + else if (foundPlugins[ dirContents[ contLoop ] ] ) + { + if( compareVersions( newestVersion, versions[ verLoop ] ) < 0 ) + { + newestVersion = versions[ verLoop ]; + } + } } - async.map( - getPlugins(context.plugins), - getPluginPackage.bind(context), - function (err, results) { - if(err) return callback(err) - loadPlugin(context, results, callback) - }) - }) - }) + } + if( newestVersion != "0.0.0" ) { foundPlugins[ dirContents[ contLoop ] ] = newestVersion; } + } + } + callback( null, foundPlugins ); + } else { + callback( "Plugin Path not found.", null ); + } + }); +} + +function load( appContext, callback) { + let makePluginPath = false; + + if ( appContext.context.hasOwnProperty( "makePluginPath" ) ) { + makePluginPath = true; + } + + if ( appContext.context.hasOwnProperty( "quiet" ) ) { + beLoud = false; + } + + let passedPath = appContext.context.hasOwnProperty( "pluginPath" ) ? appContext.context.pluginPath : ""; + var appDir = appContext.context.hasOwnProperty( "appDir") ? appContext.context.appDir : path.dirname(process.mainModule.filename); + substitutePluginPath( passedPath, appDir, function ( err, pluginPath ) { + var currentPath = path.join( pluginPath, ".current" ) + + // If the plugin path does not exist, log it and bail. + if( !fs.existsSync( pluginPath ) ) { + if ( beLoud ) { console.log( "[electron-plugins] Plugin path does not exist."); } + if ( makePluginPath ) { + mkDirByPathSync( pluginPath ); + if ( beLoud ) { console.log( "[electron-plugins] Created plugin path. "); } + } + } + else { + if ( beLoud ) { console.log( "[electron-plugins] Loading plugins from " + pluginPath ); } + var plugins; + if ( appContext.context.hasOwnProperty( "plugins" ) ) { + plugins = appContext.context.plugins; + } + else { + let contents = fs.readFileSync(currentPath, {encoding: "utf8"}) + plugins = (!err ? JSON.parse(contents) : config.plugins) || {} + } + var context = { + plugins: plugins, + pluginsDir: pluginPath, + appContext: appContext + } + async.map( + getPlugins(context.plugins), + getPluginPackage.bind(context), + function (err, results) { + if(err) return callback(err) + loadPlugin(context, results, callback) + }) + } + }) } module.exports = { - load: load -} \ No newline at end of file + load: load, + discover: discover +} diff --git a/package.json b/package.json index 21232f1..05c8832 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "homepage": "https://github.com/evolvelabs/electron-plugins", "dependencies": { "appdirectory": "^0.1.0", - "async": "^0.9.0" + "async": "^0.9.0", + "compare-versions": "^3.1.0" }, "devDependencies": { "chai": "^2.3.0",