From 300bf267f7dc6c6a6aa54aa4b49b36c28c858a83 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Fri, 19 Jan 2018 18:38:55 -0600 Subject: [PATCH 1/4] Solves for issue #6 by adding path string to the context object used on load. Also adds two booleans. One to create the plugin path if not found and another to quiet console messages. If the provided app path is empty, the default will be used. If the first character of the path is ~, that will be substitued with the results of os.homedir() If the last character of the path is :, that will be sibstituted with the App Path per ApplicationPath Both substitutions are performed with path.join. README.md updated to match. --- README.md | 10 ++++++ index.js | 91 ++++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 3845bea..e45738f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,16 @@ 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. 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. ``` diff --git a/index.js b/index.js index aac2bae..535649b 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,8 @@ var path = require('path'), async = require('async'), AppDirectory = require('appdirectory') +var beLoud = true; + function getPlugins(plugins) { var mapped = [] Object.getOwnPropertyNames(plugins).forEach(function (name) { @@ -64,9 +66,26 @@ function loadPlugin(context, results, callback) { callback(null, dependencies, modules) } -function load(appContext, callback) { - var appDir = path.dirname(process.mainModule.filename) - var packagePath = path.join(appDir, 'package.json') +function load( appContext, callback) { + let pluginPath = ''; + let makePluginPath = false; + + if ( appContext.context.hasOwnProperty( 'pluginPath' ) ) + { + pluginPath = appContext.context.pluginPath; + } + + if ( appContext.context.hasOwnProperty( 'makePluginPath' ) ) + { + makePluginPath = true; + } + + if ( appContext.context.hasOwnProperty( 'quiet' ) ) { + beLoud = false; + } + + 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) @@ -75,26 +94,56 @@ function load(appContext, callback) { 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 - } - async.map( - getPlugins(context.plugins), - getPluginPackage.bind(context), - function (err, results) { - if(err) return callback(err) - loadPlugin(context, results, callback) - }) - }) + if ( beLoud ) { console.log('[electron-plugins] appData: ' + appData); } + + // If the plugin path is not provided, use the default. + if ( !pluginPath ) { + pluginPath = path.join(appData, 'plugins'); + if ( beLoud ) { console.log( '[electron-plugins] Using default plugin path.' + pluginPath ); } + } + else { + // Check to see if this is a "relative path", ASSUME ~ is homedir for all platforms. + if( pluginPath.slice( 0, 1 ) === '~' ) + { + pluginPath = require( 'os' ).homedir() + pluginPath.substr(1); + } + if( pluginPath.slice( -1 ) === ':' ) + { + pluginPath = pluginPath.slice( 0, -1 ) + config.name; + } + if ( beLoud ) { console.log( '[electron-plugins] Using provided plugin path. ' + 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 ) { + fs.mkdirSync( pluginPath ); + if ( beLoud ) { console.log( '[electron-plugins] Created plugin path. '); } + } + } + else { + if ( beLoud ) { console.log( '[electron-plugins] Loading plugins from ' + pluginPath ); } + fs.readFile(currentPath, {encoding: 'utf8'}, function (err, contents) { + var 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 +} From ba9990dcdcf48d96b1527d201d263f544a5bdf08 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Fri, 19 Jan 2018 19:19:55 -0600 Subject: [PATCH 2/4] Missed in teh last commit. Helps to hit save. :) --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 535649b..d2d0619 100644 --- a/index.js +++ b/index.js @@ -105,11 +105,11 @@ function load( appContext, callback) { // Check to see if this is a "relative path", ASSUME ~ is homedir for all platforms. if( pluginPath.slice( 0, 1 ) === '~' ) { - pluginPath = require( 'os' ).homedir() + pluginPath.substr(1); + pluginPath = path.join( require( 'os' ).homedir(), pluginPath.substr(1) ); } if( pluginPath.slice( -1 ) === ':' ) { - pluginPath = pluginPath.slice( 0, -1 ) + config.name; + pluginPath = path.join( pluginPath.slice( 0, -1 ), config.name ); } if ( beLoud ) { console.log( '[electron-plugins] Using provided plugin path. ' + pluginPath ); } } From 24a618099311eb5a18bd207086db103814de38fe Mon Sep 17 00:00:00 2001 From: David Bolack Date: Thu, 25 Jan 2018 18:56:54 -0600 Subject: [PATCH 3/4] Added discover function to examine plugin folder for plugins. It will return either the highest installed version or the LINK version based on a passed boolean. Added plugins attribute to the context object allowing the user to provide an object of plugins from another settings store or the discovery function. Object format should match the { pluginname: version, ...} attribute scheme already used. Did a little linty clean up. Updated README.md to hopefully explain these changes. Added compare-versions depend for semvar comparisons. Reworked a little bit of shared code into common helper functions ( substitutePluginPath ) discover function does NOT load, only finds. --- README.md | 41 ++++++-- index.js | 272 ++++++++++++++++++++++++++++++++------------------- package.json | 3 +- 3 files changed, 204 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index e45738f..08c7639 100644 --- a/README.md +++ b/README.md @@ -19,25 +19,50 @@ 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: + ~ - 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 d2d0619..61b77af 100644 --- a/index.js +++ b/index.js @@ -1,149 +1,215 @@ -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); } -function load( appContext, callback) { - let pluginPath = ''; - let makePluginPath = false; +/* FROM https://stackoverflow.com/questions/31645738/how-to-create-full-path-with-nodes-fs-mkdirsync */ - if ( appContext.context.hasOwnProperty( 'pluginPath' ) ) - { - pluginPath = appContext.context.pluginPath; - } +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 ( appContext.context.hasOwnProperty( 'makePluginPath' ) ) - { - makePluginPath = true; + if ( beLoud ) { console.log(`Directory ${curDir} already exists!`); } } - if ( appContext.context.hasOwnProperty( 'quiet' ) ) { - beLoud = false; + return curDir; + }, initDir); +} + +function substitutePluginPath( passedPath, callback ) { + let pluginPath = ""; + + 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, 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, useDev, callback ) { + let foundPlugins = {}; + substitutePluginPath( pluginRelPath, 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"; + } + } + else if( compareVersions( newestVersion, versions[ verLoop ] ) < 0 ) + { + newestVersion = versions[ verLoop ]; + } + } + } + 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; + } - 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() - if ( beLoud ) { console.log('[electron-plugins] appData: ' + appData); } - - // If the plugin path is not provided, use the default. - if ( !pluginPath ) { - pluginPath = path.join(appData, 'plugins'); - if ( beLoud ) { console.log( '[electron-plugins] Using default plugin path.' + pluginPath ); } + if ( appContext.context.hasOwnProperty( "quiet" ) ) { + beLoud = false; + } + + let passedPath = appContext.context.hasOwnProperty( "pluginPath" ) ? appContext.context.pluginPath : ""; + substitutePluginPath( passedPath, 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 { - // 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) ); + } + else { + if ( beLoud ) { console.log( "[electron-plugins] Loading plugins from " + pluginPath ); } + var plugins; + if ( appContext.context.hasOwnProperty( "plugins" ) ) { + plugins = appContext.context.plugins; } - if( pluginPath.slice( -1 ) === ':' ) - { - pluginPath = path.join( pluginPath.slice( 0, -1 ), config.name ); + else { + let contents = fs.readFileSync(currentPath, {encoding: "utf8"}) + plugins = (!err ? JSON.parse(contents) : config.plugins) || {} } - if ( beLoud ) { console.log( '[electron-plugins] Using provided plugin path. ' + 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 ) { - fs.mkdirSync( pluginPath ); - if ( beLoud ) { console.log( '[electron-plugins] Created plugin path. '); } + var context = { + plugins: plugins, + pluginsDir: pluginPath, + appContext: appContext } - } - else { - if ( beLoud ) { console.log( '[electron-plugins] Loading plugins from ' + pluginPath ); } - fs.readFile(currentPath, {encoding: 'utf8'}, function (err, contents) { - var 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) - }) - }) - } - }) + async.map( + getPlugins(context.plugins), + getPluginPackage.bind(context), + function (err, results) { + if(err) return callback(err) + loadPlugin(context, results, callback) + }) + } + }) } module.exports = { - load: load + 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", From e8b847126ff632cba1e941775cd517b775f8b3d4 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Tue, 30 Jan 2018 21:55:27 -0600 Subject: [PATCH 4/4] Allow the appDir path to be passed with discover and load actions. This allows you to load plugins from teh main thread instead of a renderer. --- index.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 61b77af..85b47b3 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const async = require("async"); const AppDirectory = require("appdirectory"); const compareVersions = require( "compare-versions"); + var beLoud = true; function getPlugins(plugins) { @@ -63,7 +64,6 @@ function loadPlugin(context, results, callback) { } catch (err) { return callback(err); } - callback(null, dependencies, modules); } @@ -91,10 +91,9 @@ function mkDirByPathSync(targetDir, {isRelativeToScript = false} = {}) { }, initDir); } -function substitutePluginPath( passedPath, callback ) { +function substitutePluginPath( passedPath, appDir, callback ) { let pluginPath = ""; - var appDir = path.dirname(process.mainModule.filename); var packagePath = path.join(appDir, "package.json"); fs.readFile(packagePath, {encoding: "utf8"}, function (err, contents) { @@ -124,9 +123,10 @@ function substitutePluginPath( passedPath, callback ) { } ); } -function discover( pluginRelPath, useDev, callback ) { +function discover( pluginRelPath, appD, useDev, callback ) { let foundPlugins = {}; - substitutePluginPath( pluginRelPath, function ( err, pluginPath ) { + 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++ ) @@ -143,10 +143,14 @@ function discover( pluginRelPath, useDev, callback ) { { foundPlugins[ dirContents[ contLoop ] ] = "LINK"; } + continue; } - else if( compareVersions( newestVersion, versions[ verLoop ] ) < 0 ) + else if (foundPlugins[ dirContents[ contLoop ] ] ) { - newestVersion = versions[ verLoop ]; + if( compareVersions( newestVersion, versions[ verLoop ] ) < 0 ) + { + newestVersion = versions[ verLoop ]; + } } } } @@ -172,7 +176,8 @@ function load( appContext, callback) { } let passedPath = appContext.context.hasOwnProperty( "pluginPath" ) ? appContext.context.pluginPath : ""; - substitutePluginPath( passedPath, function ( err, 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.