diff options
Diffstat (limited to 'packages/gitbook/src/plugins')
27 files changed, 1002 insertions, 0 deletions
diff --git a/packages/gitbook/src/plugins/__tests__/findForBook.js b/packages/gitbook/src/plugins/__tests__/findForBook.js new file mode 100644 index 0000000..41df77e --- /dev/null +++ b/packages/gitbook/src/plugins/__tests__/findForBook.js @@ -0,0 +1,19 @@ +const path = require('path'); + +const Book = require('../../models/book'); +const createNodeFS = require('../../fs/node'); +const findForBook = require('../findForBook'); + +describe('findForBook', function() { + const fs = createNodeFS( + path.resolve(__dirname, '../../..') + ); + const book = Book.createForFS(fs); + + it('should list default plugins', function() { + return findForBook(book) + .then(function(plugins) { + expect(plugins.has('fontsettings')).toBeTruthy(); + }); + }); +}); diff --git a/packages/gitbook/src/plugins/__tests__/findInstalled.js b/packages/gitbook/src/plugins/__tests__/findInstalled.js new file mode 100644 index 0000000..dcaa62b --- /dev/null +++ b/packages/gitbook/src/plugins/__tests__/findInstalled.js @@ -0,0 +1,25 @@ +const path = require('path'); +const Immutable = require('immutable'); + +describe('findInstalled', function() { + const findInstalled = require('../findInstalled'); + + it('must list default plugins for gitbook directory', function() { + // Read gitbook-plugins from package.json + const pkg = require(path.resolve(__dirname, '../../../package.json')); + const gitbookPlugins = Immutable.Seq(pkg.dependencies) + .filter(function(v, k) { + return k.indexOf('gitbook-plugin') === 0; + }) + .cacheResult(); + + return findInstalled(path.resolve(__dirname, '../../../')) + .then(function(plugins) { + expect(plugins.size >= gitbookPlugins.size).toBeTruthy(); + + expect(plugins.has('fontsettings')).toBe(true); + expect(plugins.has('search')).toBe(true); + }); + }); + +}); diff --git a/packages/gitbook/src/plugins/__tests__/installPlugin.js b/packages/gitbook/src/plugins/__tests__/installPlugin.js new file mode 100644 index 0000000..1a8debe --- /dev/null +++ b/packages/gitbook/src/plugins/__tests__/installPlugin.js @@ -0,0 +1,29 @@ +const path = require('path'); + +const PluginDependency = require('../../models/pluginDependency'); +const Book = require('../../models/book'); +const NodeFS = require('../../fs/node'); +const installPlugin = require('../installPlugin'); + +const Parse = require('../../parse'); + +describe('installPlugin', function() { + let book; + + this.timeout(30000); + + before(function() { + const fs = NodeFS(path.resolve(__dirname, '../../../')); + const baseBook = Book.createForFS(fs); + + return Parse.parseConfig(baseBook) + .then(function(_book) { + book = _book; + }); + }); + + it('must install a plugin from NPM', function() { + const dep = PluginDependency.createFromString('ga'); + return installPlugin(book, dep); + }); +}); diff --git a/packages/gitbook/src/plugins/__tests__/installPlugins.js b/packages/gitbook/src/plugins/__tests__/installPlugins.js new file mode 100644 index 0000000..b6bb1f4 --- /dev/null +++ b/packages/gitbook/src/plugins/__tests__/installPlugins.js @@ -0,0 +1,30 @@ +const path = require('path'); + +const Book = require('../../models/book'); +const NodeFS = require('../../fs/node'); +const installPlugins = require('../installPlugins'); + +const Parse = require('../../parse'); + +describe('installPlugins', function() { + let book; + + this.timeout(30000); + + before(function() { + const fs = NodeFS(path.resolve(__dirname, '../../../')); + const baseBook = Book.createForFS(fs); + + return Parse.parseConfig(baseBook) + .then(function(_book) { + book = _book; + }); + }); + + it('must install all plugins from NPM', function() { + return installPlugins(book) + .then(function(n) { + expect(n).toBe(2); + }); + }); +}); diff --git a/packages/gitbook/src/plugins/__tests__/listDependencies.js b/packages/gitbook/src/plugins/__tests__/listDependencies.js new file mode 100644 index 0000000..d30e46c --- /dev/null +++ b/packages/gitbook/src/plugins/__tests__/listDependencies.js @@ -0,0 +1,38 @@ +const PluginDependency = require('../../models/pluginDependency'); +const listDependencies = require('../listDependencies'); +const toNames = require('../toNames'); + +describe('listDependencies', function() { + it('must list default', function() { + const deps = PluginDependency.listFromString('ga,great'); + const plugins = listDependencies(deps); + const names = toNames(plugins); + + expect(names).toEqual([ + 'ga', 'great', + 'highlight', 'search', 'lunr', 'sharing', 'fontsettings', + 'theme-default' ]); + }); + + it('must list from array with -', function() { + const deps = PluginDependency.listFromString('ga,-great'); + const plugins = listDependencies(deps); + const names = toNames(plugins); + + expect(names).toEqual([ + 'ga', + 'highlight', 'search', 'lunr', 'sharing', 'fontsettings', + 'theme-default' ]); + }); + + it('must remove default plugins using -', function() { + const deps = PluginDependency.listFromString('ga,-search'); + const plugins = listDependencies(deps); + const names = toNames(plugins); + + expect(names).toEqual([ + 'ga', + 'highlight', 'lunr', 'sharing', 'fontsettings', + 'theme-default' ]); + }); +}); diff --git a/packages/gitbook/src/plugins/__tests__/locateRootFolder.js b/packages/gitbook/src/plugins/__tests__/locateRootFolder.js new file mode 100644 index 0000000..54e095b --- /dev/null +++ b/packages/gitbook/src/plugins/__tests__/locateRootFolder.js @@ -0,0 +1,10 @@ +const path = require('path'); +const locateRootFolder = require('../locateRootFolder'); + +describe('locateRootFolder', function() { + it('should correctly resolve the node_modules for gitbook', function() { + expect(locateRootFolder()).toBe( + path.resolve(__dirname, '../../../') + ); + }); +}); diff --git a/packages/gitbook/src/plugins/__tests__/resolveVersion.js b/packages/gitbook/src/plugins/__tests__/resolveVersion.js new file mode 100644 index 0000000..949d078 --- /dev/null +++ b/packages/gitbook/src/plugins/__tests__/resolveVersion.js @@ -0,0 +1,22 @@ +const PluginDependency = require('../../models/pluginDependency'); +const resolveVersion = require('../resolveVersion'); + +describe('resolveVersion', function() { + it('must skip resolving and return non-semver versions', function() { + const plugin = PluginDependency.createFromString('ga@git+ssh://samy@github.com/GitbookIO/plugin-ga.git'); + + return resolveVersion(plugin) + .then(function(version) { + expect(version).toBe('git+ssh://samy@github.com/GitbookIO/plugin-ga.git'); + }); + }); + + it('must resolve a normal plugin dependency', function() { + const plugin = PluginDependency.createFromString('ga@>0.9.0 < 1.0.1'); + + return resolveVersion(plugin) + .then(function(version) { + expect(version).toBe('1.0.0'); + }); + }); +}); diff --git a/packages/gitbook/src/plugins/__tests__/sortDependencies.js b/packages/gitbook/src/plugins/__tests__/sortDependencies.js new file mode 100644 index 0000000..a08d59d --- /dev/null +++ b/packages/gitbook/src/plugins/__tests__/sortDependencies.js @@ -0,0 +1,42 @@ +const PluginDependency = require('../../models/pluginDependency'); +const sortDependencies = require('../sortDependencies'); +const toNames = require('../toNames'); + +describe('sortDependencies', function() { + it('must load themes after plugins', function() { + const allPlugins = PluginDependency.listFromArray([ + 'hello', + 'theme-test', + 'world' + ]); + + const sorted = sortDependencies(allPlugins); + const names = toNames(sorted); + + expect(names).toEqual([ + 'hello', + 'world', + 'theme-test' + ]); + }); + + it('must keep order of themes', function() { + const allPlugins = PluginDependency.listFromArray([ + 'theme-test', + 'theme-test1', + 'hello', + 'theme-test2', + 'world' + ]); + const sorted = sortDependencies(allPlugins); + const names = toNames(sorted); + + expect(names).toEqual([ + 'hello', + 'world', + 'theme-test', + 'theme-test1', + 'theme-test2' + ]); + }); +}); diff --git a/packages/gitbook/src/plugins/__tests__/validatePlugin.js b/packages/gitbook/src/plugins/__tests__/validatePlugin.js new file mode 100644 index 0000000..a2bd23b --- /dev/null +++ b/packages/gitbook/src/plugins/__tests__/validatePlugin.js @@ -0,0 +1,16 @@ +const Promise = require('../../utils/promise'); +const Plugin = require('../../models/plugin'); +const validatePlugin = require('../validatePlugin'); + +describe('validatePlugin', function() { + it('must not validate a not loaded plugin', function() { + const plugin = Plugin.createFromString('test'); + + return validatePlugin(plugin) + .then(function() { + throw new Error('Should not be validate'); + }, function(err) { + return Promise(); + }); + }); +}); diff --git a/packages/gitbook/src/plugins/findForBook.js b/packages/gitbook/src/plugins/findForBook.js new file mode 100644 index 0000000..b72d526 --- /dev/null +++ b/packages/gitbook/src/plugins/findForBook.js @@ -0,0 +1,34 @@ +const Immutable = require('immutable'); + +const Promise = require('../utils/promise'); +const timing = require('../utils/timing'); +const findInstalled = require('./findInstalled'); +const locateRootFolder = require('./locateRootFolder'); + +/** + * List all plugins installed in a book + * + * @param {Book} + * @return {Promise<OrderedMap<String:Plugin>>} + */ +function findForBook(book) { + return timing.measure( + 'plugins.findForBook', + + Promise.all([ + findInstalled(locateRootFolder()), + findInstalled(book.getRoot()) + ]) + + // Merge all plugins + .then(function(results) { + return Immutable.List(results) + .reduce(function(out, result) { + return out.merge(result); + }, Immutable.OrderedMap()); + }) + ); +} + + +module.exports = findForBook; diff --git a/packages/gitbook/src/plugins/findInstalled.js b/packages/gitbook/src/plugins/findInstalled.js new file mode 100644 index 0000000..15556b6 --- /dev/null +++ b/packages/gitbook/src/plugins/findInstalled.js @@ -0,0 +1,91 @@ +const readInstalled = require('read-installed'); +const Immutable = require('immutable'); +const path = require('path'); + +const Promise = require('../utils/promise'); +const fs = require('../utils/fs'); +const Plugin = require('../models/plugin'); +const PREFIX = require('../constants/pluginPrefix'); + +/** + * Validate if a package name is a GitBook plugin + * + * @return {Boolean} + */ +function validateId(name) { + return name && name.indexOf(PREFIX) === 0; +} + + +/** + * List all packages installed inside a folder + * + * @param {String} folder + * @return {OrderedMap<String:Plugin>} + */ +function findInstalled(folder) { + const options = { + dev: false, + log() {}, + depth: 4 + }; + let results = Immutable.OrderedMap(); + + function onPackage(pkg, parent) { + if (!pkg.name) return; + + const name = pkg.name; + const version = pkg.version; + const pkgPath = pkg.realPath; + const depth = pkg.depth; + const dependencies = pkg.dependencies; + + const pluginName = name.slice(PREFIX.length); + + if (!validateId(name)) { + if (parent) return; + } else { + results = results.set(pluginName, Plugin({ + name: pluginName, + version, + path: pkgPath, + depth, + parent + })); + } + + Immutable.Map(dependencies).forEach(function(dep) { + onPackage(dep, pluginName); + }); + } + + // Search for gitbook-plugins in node_modules folder + const node_modules = path.join(folder, 'node_modules'); + + // List all folders in node_modules + return fs.readdir(node_modules) + .fail(function() { + return Promise([]); + }) + .then(function(modules) { + return Promise.serie(modules, function(module) { + // Not a gitbook-plugin + if (!validateId(module)) { + return Promise(); + } + + // Read gitbook-plugin package details + const module_folder = path.join(node_modules, module); + return Promise.nfcall(readInstalled, module_folder, options) + .then(function(data) { + onPackage(data); + }); + }); + }) + .then(function() { + // Return installed plugins + return results; + }); +} + +module.exports = findInstalled; diff --git a/packages/gitbook/src/plugins/index.js b/packages/gitbook/src/plugins/index.js new file mode 100644 index 0000000..607a7f1 --- /dev/null +++ b/packages/gitbook/src/plugins/index.js @@ -0,0 +1,10 @@ + +module.exports = { + loadForBook: require('./loadForBook'), + validateConfig: require('./validateConfig'), + installPlugins: require('./installPlugins'), + listResources: require('./listResources'), + listBlocks: require('./listBlocks'), + listFilters: require('./listFilters') +}; + diff --git a/packages/gitbook/src/plugins/installPlugin.js b/packages/gitbook/src/plugins/installPlugin.js new file mode 100644 index 0000000..edf4dc5 --- /dev/null +++ b/packages/gitbook/src/plugins/installPlugin.js @@ -0,0 +1,47 @@ +const npmi = require('npmi'); + +const Promise = require('../utils/promise'); +const resolveVersion = require('./resolveVersion'); + +/** + Install a plugin for a book + + @param {Book} + @param {PluginDependency} + @return {Promise} +*/ +function installPlugin(book, plugin) { + const logger = book.getLogger(); + + const installFolder = book.getRoot(); + const name = plugin.getName(); + const requirement = plugin.getVersion(); + + logger.info.ln(''); + logger.info.ln('installing plugin "' + name + '"'); + + // Find a version to install + return resolveVersion(plugin) + .then(function(version) { + if (!version) { + throw new Error('Found no satisfactory version for plugin "' + name + '" with requirement "' + requirement + '"'); + } + + logger.info.ln('install plugin "' + name + '" (' + requirement + ') from NPM with version', version); + return Promise.nfcall(npmi, { + 'name': plugin.getNpmID(), + version, + 'path': installFolder, + 'npmLoad': { + 'loglevel': 'silent', + 'loaded': true, + 'prefix': installFolder + } + }); + }) + .then(function() { + logger.info.ok('plugin "' + name + '" installed with success'); + }); +} + +module.exports = installPlugin; diff --git a/packages/gitbook/src/plugins/installPlugins.js b/packages/gitbook/src/plugins/installPlugins.js new file mode 100644 index 0000000..8c36c92 --- /dev/null +++ b/packages/gitbook/src/plugins/installPlugins.js @@ -0,0 +1,48 @@ +const npmi = require('npmi'); + +const DEFAULT_PLUGINS = require('../constants/defaultPlugins'); +const Promise = require('../utils/promise'); +const installPlugin = require('./installPlugin'); + +/** + Install plugin requirements for a book + + @param {Book} + @return {Promise<Number>} +*/ +function installPlugins(book) { + const logger = book.getLogger(); + const config = book.getConfig(); + let plugins = config.getPluginDependencies(); + + // Remove default plugins + // (only if version is same as installed) + plugins = plugins.filterNot(function(plugin) { + const dependency = DEFAULT_PLUGINS.find(function(dep) { + return dep.getName() === plugin.getName(); + }); + + return ( + // Disabled plugin + !plugin.isEnabled() || + + // Or default one installed in GitBook itself + (dependency && + plugin.getVersion() === dependency.getVersion()) + ); + }); + + if (plugins.size == 0) { + logger.info.ln('nothing to install!'); + return Promise(); + } + + logger.info.ln('installing', plugins.size, 'plugins using npm@' + npmi.NPM_VERSION); + + return Promise.forEach(plugins, function(plugin) { + return installPlugin(book, plugin); + }) + .thenResolve(plugins.size); +} + +module.exports = installPlugins; diff --git a/packages/gitbook/src/plugins/listBlocks.js b/packages/gitbook/src/plugins/listBlocks.js new file mode 100644 index 0000000..991b386 --- /dev/null +++ b/packages/gitbook/src/plugins/listBlocks.js @@ -0,0 +1,18 @@ +const Immutable = require('immutable'); + +/** + List blocks from a list of plugins + + @param {OrderedMap<String:Plugin>} + @return {Map<String:TemplateBlock>} +*/ +function listBlocks(plugins) { + return plugins + .reverse() + .reduce(function(result, plugin) { + const blocks = plugin.getBlocks(); + return result.merge(blocks); + }, Immutable.Map()); +} + +module.exports = listBlocks; diff --git a/packages/gitbook/src/plugins/listDependencies.js b/packages/gitbook/src/plugins/listDependencies.js new file mode 100644 index 0000000..3930ae7 --- /dev/null +++ b/packages/gitbook/src/plugins/listDependencies.js @@ -0,0 +1,33 @@ +const DEFAULT_PLUGINS = require('../constants/defaultPlugins'); +const sortDependencies = require('./sortDependencies'); + +/** + * List all dependencies for a book, including default plugins. + * It returns a concat with default plugins and remove disabled ones. + * + * @param {List<PluginDependency>} deps + * @return {List<PluginDependency>} + */ +function listDependencies(deps) { + // Extract list of plugins to disable (starting with -) + const toRemove = deps + .filter(function(plugin) { + return !plugin.isEnabled(); + }) + .map(function(plugin) { + return plugin.getName(); + }); + + // Concat with default plugins + deps = deps.concat(DEFAULT_PLUGINS); + + // Remove plugins + deps = deps.filterNot(function(plugin) { + return toRemove.includes(plugin.getName()); + }); + + // Sort + return sortDependencies(deps); +} + +module.exports = listDependencies; diff --git a/packages/gitbook/src/plugins/listDepsForBook.js b/packages/gitbook/src/plugins/listDepsForBook.js new file mode 100644 index 0000000..b173572 --- /dev/null +++ b/packages/gitbook/src/plugins/listDepsForBook.js @@ -0,0 +1,18 @@ +const listDependencies = require('./listDependencies'); + +/** + * List all plugin requirements for a book. + * It can be different from the final list of plugins, + * since plugins can have their own dependencies + * + * @param {Book} + * @return {List<PluginDependency>} + */ +function listDepsForBook(book) { + const config = book.getConfig(); + const plugins = config.getPluginDependencies(); + + return listDependencies(plugins); +} + +module.exports = listDepsForBook; diff --git a/packages/gitbook/src/plugins/listFilters.js b/packages/gitbook/src/plugins/listFilters.js new file mode 100644 index 0000000..edf6c0d --- /dev/null +++ b/packages/gitbook/src/plugins/listFilters.js @@ -0,0 +1,17 @@ +const Immutable = require('immutable'); + +/** + List filters from a list of plugins + + @param {OrderedMap<String:Plugin>} + @return {Map<String:Function>} +*/ +function listFilters(plugins) { + return plugins + .reverse() + .reduce(function(result, plugin) { + return result.merge(plugin.getFilters()); + }, Immutable.Map()); +} + +module.exports = listFilters; diff --git a/packages/gitbook/src/plugins/listResources.js b/packages/gitbook/src/plugins/listResources.js new file mode 100644 index 0000000..df50097 --- /dev/null +++ b/packages/gitbook/src/plugins/listResources.js @@ -0,0 +1,45 @@ +const Immutable = require('immutable'); +const path = require('path'); + +const LocationUtils = require('../utils/location'); +const PLUGIN_RESOURCES = require('../constants/pluginResources'); + +/** + List all resources from a list of plugins + + @param {OrderedMap<String:Plugin>} + @param {String} type + @return {Map<String:List<{url, path}>} +*/ +function listResources(plugins, resources) { + return plugins.reduce(function(result, plugin) { + const npmId = plugin.getNpmID(); + const pluginResources = resources.get(plugin.getName()); + + PLUGIN_RESOURCES.forEach(function(resourceType) { + let assets = pluginResources.get(resourceType); + if (!assets) return; + + let list = result.get(resourceType) || Immutable.List(); + + assets = assets.map(function(assetFile) { + if (LocationUtils.isExternal(assetFile)) { + return { + url: assetFile + }; + } else { + return { + path: LocationUtils.normalize(path.join(npmId, assetFile)) + }; + } + }); + + list = list.concat(assets); + result = result.set(resourceType, list); + }); + + return result; + }, Immutable.Map()); +} + +module.exports = listResources; diff --git a/packages/gitbook/src/plugins/loadForBook.js b/packages/gitbook/src/plugins/loadForBook.js new file mode 100644 index 0000000..0baa78e --- /dev/null +++ b/packages/gitbook/src/plugins/loadForBook.js @@ -0,0 +1,73 @@ +const Immutable = require('immutable'); + +const Promise = require('../utils/promise'); +const listDepsForBook = require('./listDepsForBook'); +const findForBook = require('./findForBook'); +const loadPlugin = require('./loadPlugin'); + + +/** + * Load all plugins in a book + * + * @param {Book} + * @return {Promise<Map<String:Plugin>} + */ +function loadForBook(book) { + const logger = book.getLogger(); + + // List the dependencies + const requirements = listDepsForBook(book); + + // List all plugins installed in the book + return findForBook(book) + .then(function(installedMap) { + const missing = []; + let plugins = requirements.reduce(function(result, dep) { + const name = dep.getName(); + const installed = installedMap.get(name); + + if (installed) { + const deps = installedMap + .filter(function(plugin) { + return plugin.getParent() === name; + }) + .toArray(); + + result = result.concat(deps); + result.push(installed); + } else { + missing.push(name); + } + + return result; + }, []); + + // Convert plugins list to a map + plugins = Immutable.List(plugins) + .map(function(plugin) { + return [ + plugin.getName(), + plugin + ]; + }); + plugins = Immutable.OrderedMap(plugins); + + // Log state + logger.info.ln(installedMap.size + ' plugins are installed'); + if (requirements.size != installedMap.size) { + logger.info.ln(requirements.size + ' explicitly listed'); + } + + // Verify that all plugins are present + if (missing.length > 0) { + throw new Error('Couldn\'t locate plugins "' + missing.join(', ') + '", Run \'gitbook install\' to install plugins from registry.'); + } + + return Promise.map(plugins, function(plugin) { + return loadPlugin(book, plugin); + }); + }); +} + + +module.exports = loadForBook; diff --git a/packages/gitbook/src/plugins/loadPlugin.js b/packages/gitbook/src/plugins/loadPlugin.js new file mode 100644 index 0000000..4a349e2 --- /dev/null +++ b/packages/gitbook/src/plugins/loadPlugin.js @@ -0,0 +1,89 @@ +const path = require('path'); +const resolve = require('resolve'); +const Immutable = require('immutable'); + +const Promise = require('../utils/promise'); +const error = require('../utils/error'); +const timing = require('../utils/timing'); + +const validatePlugin = require('./validatePlugin'); + +// Return true if an error is a "module not found" +// Wait on https://github.com/substack/node-resolve/pull/81 to be merged +function isModuleNotFound(err) { + return err.code == 'MODULE_NOT_FOUND' || err.message.indexOf('Cannot find module') >= 0; +} + +/** + Load a plugin in a book + + @param {Book} book + @param {Plugin} plugin + @param {String} pkgPath (optional) + @return {Promise<Plugin>} +*/ +function loadPlugin(book, plugin) { + const logger = book.getLogger(); + + const name = plugin.getName(); + let pkgPath = plugin.getPath(); + + // Try loading plugins from different location + let p = Promise() + .then(function() { + let packageContent; + let packageMain; + let content; + + // Locate plugin and load package.json + try { + const res = resolve.sync('./package.json', { basedir: pkgPath }); + + pkgPath = path.dirname(res); + packageContent = require(res); + } catch (err) { + if (!isModuleNotFound(err)) throw err; + + packageContent = undefined; + content = undefined; + + return; + } + + // Locate the main package + try { + const indexJs = path.normalize(packageContent.main || 'index.js'); + packageMain = resolve.sync('./' + indexJs, { basedir: pkgPath }); + } catch (err) { + if (!isModuleNotFound(err)) throw err; + packageMain = undefined; + } + + // Load plugin JS content + if (packageMain) { + try { + content = require(packageMain); + } catch (err) { + throw new error.PluginError(err, { + plugin: name + }); + } + } + + // Update plugin + return plugin.merge({ + 'package': Immutable.fromJS(packageContent), + 'content': Immutable.fromJS(content || {}) + }); + }) + + .then(validatePlugin); + + p = timing.measure('plugin.load', p); + + logger.info('loading plugin "' + name + '"... '); + return logger.info.promise(p); +} + + +module.exports = loadPlugin; diff --git a/packages/gitbook/src/plugins/locateRootFolder.js b/packages/gitbook/src/plugins/locateRootFolder.js new file mode 100644 index 0000000..64e06a8 --- /dev/null +++ b/packages/gitbook/src/plugins/locateRootFolder.js @@ -0,0 +1,22 @@ +const path = require('path'); +const resolve = require('resolve'); + +const DEFAULT_PLUGINS = require('../constants/defaultPlugins'); + +/** + * Resolve the root folder containing for node_modules + * since gitbook can be used as a library and dependency can be flattened. + * + * @return {String} folderPath + */ +function locateRootFolder() { + const firstDefaultPlugin = DEFAULT_PLUGINS.first(); + const pluginPath = resolve.sync(firstDefaultPlugin.getNpmID() + '/package.json', { + basedir: __dirname + }); + const nodeModules = path.resolve(pluginPath, '../../..'); + + return nodeModules; +} + +module.exports = locateRootFolder; diff --git a/packages/gitbook/src/plugins/resolveVersion.js b/packages/gitbook/src/plugins/resolveVersion.js new file mode 100644 index 0000000..07b771e --- /dev/null +++ b/packages/gitbook/src/plugins/resolveVersion.js @@ -0,0 +1,71 @@ +const npm = require('npm'); +const semver = require('semver'); +const Immutable = require('immutable'); + +const Promise = require('../utils/promise'); +const Plugin = require('../models/plugin'); +const gitbook = require('../gitbook'); + +let npmIsReady; + +/** + Initialize and prepare NPM + + @return {Promise} +*/ +function initNPM() { + if (npmIsReady) return npmIsReady; + + npmIsReady = Promise.nfcall(npm.load, { + silent: true, + loglevel: 'silent' + }); + + return npmIsReady; +} + +/** + Resolve a plugin dependency to a version + + @param {PluginDependency} plugin + @return {Promise<String>} +*/ +function resolveVersion(plugin) { + const npmId = Plugin.nameToNpmID(plugin.getName()); + const requiredVersion = plugin.getVersion(); + + if (plugin.isGitDependency()) { + return Promise.resolve(requiredVersion); + } + + return initNPM() + .then(function() { + return Promise.nfcall(npm.commands.view, [npmId + '@' + requiredVersion, 'engines'], true); + }) + .then(function(versions) { + versions = Immutable.Map(versions).entrySeq(); + + const result = versions + .map(function(entry) { + return { + version: entry[0], + gitbook: (entry[1].engines || {}).gitbook + }; + }) + .filter(function(v) { + return v.gitbook && gitbook.satisfies(v.gitbook); + }) + .sort(function(v1, v2) { + return semver.lt(v1.version, v2.version) ? 1 : -1; + }) + .get(0); + + if (!result) { + return undefined; + } else { + return result.version; + } + }); +} + +module.exports = resolveVersion; diff --git a/packages/gitbook/src/plugins/sortDependencies.js b/packages/gitbook/src/plugins/sortDependencies.js new file mode 100644 index 0000000..2adfa20 --- /dev/null +++ b/packages/gitbook/src/plugins/sortDependencies.js @@ -0,0 +1,34 @@ +const Immutable = require('immutable'); + +const THEME_PREFIX = require('../constants/themePrefix'); + +const TYPE_PLUGIN = 'plugin'; +const TYPE_THEME = 'theme'; + + +/** + * Returns the type of a plugin given its name + * @param {Plugin} plugin + * @return {String} + */ +function pluginType(plugin) { + const name = plugin.getName(); + return (name && name.indexOf(THEME_PREFIX) === 0) ? TYPE_THEME : TYPE_PLUGIN; +} + + +/** + * Sort the list of dependencies to match list in book.json + * The themes should always be loaded after the plugins + * + * @param {List<PluginDependency>} deps + * @return {List<PluginDependency>} + */ +function sortDependencies(plugins) { + const byTypes = plugins.groupBy(pluginType); + + return byTypes.get(TYPE_PLUGIN, Immutable.List()) + .concat(byTypes.get(TYPE_THEME, Immutable.List())); +} + +module.exports = sortDependencies; diff --git a/packages/gitbook/src/plugins/toNames.js b/packages/gitbook/src/plugins/toNames.js new file mode 100644 index 0000000..ad0dd8f --- /dev/null +++ b/packages/gitbook/src/plugins/toNames.js @@ -0,0 +1,16 @@ + +/** + * Return list of plugin names. This method is nly used in unit tests. + * + * @param {OrderedMap<String:Plugin} plugins + * @return {Array<String>} + */ +function toNames(plugins) { + return plugins + .map(function(plugin) { + return plugin.getName(); + }) + .toArray(); +} + +module.exports = toNames; diff --git a/packages/gitbook/src/plugins/validateConfig.js b/packages/gitbook/src/plugins/validateConfig.js new file mode 100644 index 0000000..8e24775 --- /dev/null +++ b/packages/gitbook/src/plugins/validateConfig.js @@ -0,0 +1,71 @@ +const Immutable = require('immutable'); +const jsonschema = require('jsonschema'); +const jsonSchemaDefaults = require('json-schema-defaults'); + +const Promise = require('../utils/promise'); +const error = require('../utils/error'); +const mergeDefaults = require('../utils/mergeDefaults'); + +/** + Validate one plugin for a book and update book's confiration + + @param {Book} + @param {Plugin} + @return {Book} +*/ +function validatePluginConfig(book, plugin) { + let config = book.getConfig(); + const packageInfos = plugin.getPackage(); + + const configKey = [ + 'pluginsConfig', + plugin.getName() + ].join('.'); + + let pluginConfig = config.getValue(configKey, {}).toJS(); + + const schema = (packageInfos.get('gitbook') || Immutable.Map()).toJS(); + if (!schema) return book; + + // Normalize schema + schema.id = '/' + configKey; + schema.type = 'object'; + + // Validate and throw if invalid + const v = new jsonschema.Validator(); + const result = v.validate(pluginConfig, schema, { + propertyName: configKey + }); + + // Throw error + if (result.errors.length > 0) { + throw new error.ConfigurationError(new Error(result.errors[0].stack)); + } + + // Insert default values + const defaults = jsonSchemaDefaults(schema); + pluginConfig = mergeDefaults(pluginConfig, defaults); + + + // Update configuration + config = config.setValue(configKey, pluginConfig); + + // Return new book + return book.set('config', config); +} + +/** + Validate a book configuration for plugins and + returns an update configuration with default values. + + @param {Book} + @param {OrderedMap<String:Plugin>} + @return {Promise<Book>} +*/ +function validateConfig(book, plugins) { + return Promise.reduce(plugins, function(newBook, plugin) { + return validatePluginConfig(newBook, plugin); + }, book); +} + +module.exports = validateConfig; diff --git a/packages/gitbook/src/plugins/validatePlugin.js b/packages/gitbook/src/plugins/validatePlugin.js new file mode 100644 index 0000000..f0e96ba --- /dev/null +++ b/packages/gitbook/src/plugins/validatePlugin.js @@ -0,0 +1,34 @@ +const gitbook = require('../gitbook'); + +const Promise = require('../utils/promise'); + +/** + Validate a plugin + + @param {Plugin} + @return {Promise<Plugin>} +*/ +function validatePlugin(plugin) { + const packageInfos = plugin.getPackage(); + + const isValid = ( + plugin.isLoaded() && + packageInfos && + packageInfos.get('name') && + packageInfos.get('engines') && + packageInfos.get('engines').get('gitbook') + ); + + if (!isValid) { + return Promise.reject(new Error('Error loading plugin "' + plugin.getName() + '" at "' + plugin.getPath() + '"')); + } + + const engine = packageInfos.get('engines').get('gitbook'); + if (!gitbook.satisfies(engine)) { + return Promise.reject(new Error('GitBook doesn\'t satisfy the requirements of this plugin: ' + engine)); + } + + return Promise(plugin); +} + +module.exports = validatePlugin; |