summaryrefslogtreecommitdiffstats
path: root/packages/gitbook/src/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'packages/gitbook/src/plugins')
-rw-r--r--packages/gitbook/src/plugins/__tests__/findForBook.js19
-rw-r--r--packages/gitbook/src/plugins/__tests__/findInstalled.js25
-rw-r--r--packages/gitbook/src/plugins/__tests__/installPlugin.js29
-rw-r--r--packages/gitbook/src/plugins/__tests__/installPlugins.js30
-rw-r--r--packages/gitbook/src/plugins/__tests__/listDependencies.js38
-rw-r--r--packages/gitbook/src/plugins/__tests__/locateRootFolder.js10
-rw-r--r--packages/gitbook/src/plugins/__tests__/resolveVersion.js22
-rw-r--r--packages/gitbook/src/plugins/__tests__/sortDependencies.js42
-rw-r--r--packages/gitbook/src/plugins/__tests__/validatePlugin.js16
-rw-r--r--packages/gitbook/src/plugins/findForBook.js34
-rw-r--r--packages/gitbook/src/plugins/findInstalled.js91
-rw-r--r--packages/gitbook/src/plugins/index.js10
-rw-r--r--packages/gitbook/src/plugins/installPlugin.js47
-rw-r--r--packages/gitbook/src/plugins/installPlugins.js48
-rw-r--r--packages/gitbook/src/plugins/listBlocks.js18
-rw-r--r--packages/gitbook/src/plugins/listDependencies.js33
-rw-r--r--packages/gitbook/src/plugins/listDepsForBook.js18
-rw-r--r--packages/gitbook/src/plugins/listFilters.js17
-rw-r--r--packages/gitbook/src/plugins/listResources.js45
-rw-r--r--packages/gitbook/src/plugins/loadForBook.js73
-rw-r--r--packages/gitbook/src/plugins/loadPlugin.js89
-rw-r--r--packages/gitbook/src/plugins/locateRootFolder.js22
-rw-r--r--packages/gitbook/src/plugins/resolveVersion.js71
-rw-r--r--packages/gitbook/src/plugins/sortDependencies.js34
-rw-r--r--packages/gitbook/src/plugins/toNames.js16
-rw-r--r--packages/gitbook/src/plugins/validateConfig.js71
-rw-r--r--packages/gitbook/src/plugins/validatePlugin.js34
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;