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.js42
-rw-r--r--packages/gitbook/src/plugins/__tests__/installPlugins.js37
-rw-r--r--packages/gitbook/src/plugins/__tests__/listDependencies.js39
-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.js33
-rw-r--r--packages/gitbook/src/plugins/findInstalled.js81
-rw-r--r--packages/gitbook/src/plugins/index.js8
-rw-r--r--packages/gitbook/src/plugins/installPlugin.js44
-rw-r--r--packages/gitbook/src/plugins/installPlugins.js46
-rw-r--r--packages/gitbook/src/plugins/listBlocks.js21
-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.js20
-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.js70
-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
26 files changed, 965 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..0d12aa1
--- /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', () => {
+ const fs = createNodeFS(
+ path.resolve(__dirname, '../../..')
+ );
+ const book = Book.createForFS(fs);
+
+ it('should list default plugins', () => {
+ return findForBook(book)
+ .then((plugins) => {
+ expect(plugins.has('theme-default')).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..e787761
--- /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('highlight')).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..97f1475
--- /dev/null
+++ b/packages/gitbook/src/plugins/__tests__/installPlugin.js
@@ -0,0 +1,42 @@
+const tmp = require('tmp');
+
+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', () => {
+ let book, dir;
+
+ before(() => {
+ dir = tmp.dirSync({ unsafeCleanup: true });
+ const fs = NodeFS(dir.name);
+ const baseBook = Book.createForFS(fs)
+ .setLogLevel('disabled');
+
+ return Parse.parseConfig(baseBook)
+ .then((_book) => {
+ book = _book;
+ });
+ });
+
+ after(() => {
+ dir.removeCallback();
+ });
+
+ it('must install a plugin from NPM', () => {
+ const dep = PluginDependency.createFromString('ga');
+ return installPlugin(book, dep)
+ .then(() => {
+ expect(dir.name).toHaveFile('node_modules/gitbook-plugin-ga/package.json');
+ expect(dir.name).toNotHaveFile('package.json');
+ });
+ });
+
+ it('must install a specific version of a plugin', () => {
+ const dep = PluginDependency.createFromString('ga@0.2.1');
+ 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..26f135d
--- /dev/null
+++ b/packages/gitbook/src/plugins/__tests__/installPlugins.js
@@ -0,0 +1,37 @@
+const tmp = require('tmp');
+
+const Book = require('../../models/book');
+const MockFS = require('../../fs/mock');
+const installPlugins = require('../installPlugins');
+
+const Parse = require('../../parse');
+
+describe('installPlugins', () => {
+ let book, dir;
+
+ before(() => {
+ dir = tmp.dirSync({ unsafeCleanup: true });
+
+ const fs = MockFS({
+ 'book.json': JSON.stringify({ plugins: ['ga', 'sitemap' ]})
+ }, dir.name);
+ const baseBook = Book.createForFS(fs)
+ .setLogLevel('disabled');
+
+ return Parse.parseConfig(baseBook)
+ .then((_book) => {
+ book = _book;
+ });
+ });
+
+ after(() => {
+ dir.removeCallback();
+ });
+
+ it('must install all plugins from NPM', () => {
+ 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..002f0e9
--- /dev/null
+++ b/packages/gitbook/src/plugins/__tests__/listDependencies.js
@@ -0,0 +1,39 @@
+const PluginDependency = require('../../models/pluginDependency');
+const listDependencies = require('../listDependencies');
+const toNames = require('../toNames');
+
+describe('listDependencies', () => {
+ it('must list default', () => {
+ const deps = PluginDependency.listFromString('ga,great');
+ const plugins = listDependencies(deps);
+ const names = toNames(plugins);
+
+ expect(names).toEqual([
+ 'ga', 'great', 'highlight', 'search', 'lunr',
+ 'sharing', 'hints', 'headings', 'copy-code', 'theme-default'
+ ]);
+ });
+
+ it('must list from array with -', () => {
+ const deps = PluginDependency.listFromString('ga,-great');
+ const plugins = listDependencies(deps);
+ const names = toNames(plugins);
+
+ expect(names).toEqual([
+ 'ga', 'highlight', 'search', 'lunr',
+ 'sharing', 'hints', 'headings',
+ 'copy-code', 'theme-default'
+ ]);
+ });
+
+ it('must remove default plugins using -', () => {
+ const deps = PluginDependency.listFromString('ga,-search');
+ const plugins = listDependencies(deps);
+ const names = toNames(plugins);
+
+ expect(names).toEqual([
+ 'ga', 'highlight', 'lunr', 'sharing',
+ 'hints', 'headings', 'copy-code', '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..8668d1d
--- /dev/null
+++ b/packages/gitbook/src/plugins/findForBook.js
@@ -0,0 +1,33 @@
+const { List, OrderedMap } = 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 List(results)
+ .reduce(function(out, result) {
+ return out.merge(result);
+ }, 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..fb690c2
--- /dev/null
+++ b/packages/gitbook/src/plugins/findInstalled.js
@@ -0,0 +1,81 @@
+const { OrderedMap } = 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;
+}
+
+/**
+ * Read details about a node module.
+ * @param {String} modulePath
+ * @param {Number} depth
+ * @param {String} parent
+ * @return {Plugin} plugin
+ */
+function readModule(modulePath, depth, parent) {
+ const pkg = require(path.join(modulePath, 'package.json'));
+ const pluginName = pkg.name.slice(PREFIX.length);
+
+ return new Plugin({
+ name: pluginName,
+ version: pkg.version,
+ path: modulePath,
+ depth,
+ parent
+ });
+}
+
+/**
+ * List all packages installed inside a folder
+ *
+ * @param {String} folder
+ * @param {Number} depth
+ * @param {String} parent
+ * @return {Promise<OrderedMap<String:Plugin>>} plugins
+ */
+function findInstalled(folder, depth = 0, parent = null) {
+ // When tetsing with mock-fs
+ if (!folder) {
+ return Promise(OrderedMap());
+ }
+
+ // 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(() => {
+ return Promise([]);
+ })
+ .then((modules) => {
+ return Promise.reduce(modules, (results, moduleName) => {
+ // Not a gitbook-plugin
+ if (!validateId(moduleName)) {
+ return results;
+ }
+
+ // Read gitbook-plugin package details
+ const moduleFolder = path.join(node_modules, moduleName);
+ const plugin = readModule(moduleFolder, depth, parent);
+
+ results = results.set(plugin.getName(), plugin);
+
+ return findInstalled(moduleFolder, depth + 1, plugin.getName())
+ .then((innerModules) => {
+ return results.merge(innerModules);
+ });
+ }, OrderedMap());
+ });
+}
+
+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..bdc3b05
--- /dev/null
+++ b/packages/gitbook/src/plugins/index.js
@@ -0,0 +1,8 @@
+
+module.exports = {
+ loadForBook: require('./loadForBook'),
+ validateConfig: require('./validateConfig'),
+ installPlugins: require('./installPlugins'),
+ 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..9834d05
--- /dev/null
+++ b/packages/gitbook/src/plugins/installPlugin.js
@@ -0,0 +1,44 @@
+const resolve = require('resolve');
+
+const { exec } = require('../utils/command');
+const resolveVersion = require('./resolveVersion');
+
+/**
+ * Install a plugin for a book
+ *
+ * @param {Book} book
+ * @param {PluginDependency} plugin
+ * @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 + '"');
+
+ const installerBin = resolve.sync('ied/lib/cmd.js');
+
+ // 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 + ') with version', version);
+
+ const npmID = plugin.getNpmID();
+ const command = `${installerBin} install ${npmID}@${version}`;
+
+ return exec(command, { cwd: 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..9d2520f
--- /dev/null
+++ b/packages/gitbook/src/plugins/installPlugins.js
@@ -0,0 +1,46 @@
+const DEFAULT_PLUGINS = require('../constants/defaultPlugins');
+const Promise = require('../utils/promise');
+const installPlugin = require('./installPlugin');
+
+/**
+ * Install plugin requirements for a book
+ *
+ * @param {Book} book
+ * @return {Promise<Number>} count
+ */
+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(0);
+ }
+
+ logger.info.ln('installing', plugins.size, 'plugins from registry');
+
+ 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..a2b04f5
--- /dev/null
+++ b/packages/gitbook/src/plugins/listBlocks.js
@@ -0,0 +1,21 @@
+const { Map } = require('immutable');
+
+/**
+ * List blocks from a list of plugins
+ *
+ * @param {OrderedMap<String:Plugin>}
+ * @return {Map<String:TemplateBlock>}
+ */
+function listBlocks(plugins) {
+ return plugins
+ .reverse()
+ .reduce(
+ (result, plugin) => {
+ const blocks = plugin.getBlocks();
+ return result.merge(blocks);
+ },
+ 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..81f619d
--- /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} book
+ * @return {List<PluginDependency>} dependencies
+ */
+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..57d5c29
--- /dev/null
+++ b/packages/gitbook/src/plugins/listFilters.js
@@ -0,0 +1,20 @@
+const { Map } = require('immutable');
+
+/**
+ * List filters from a list of plugins
+ *
+ * @param {OrderedMap<String:Plugin>} plugins
+ * @return {Map<String:Function>} filters
+ */
+function listFilters(plugins) {
+ return plugins
+ .reverse()
+ .reduce(
+ (result, plugin) => {
+ return result.merge(plugin.getFilters());
+ },
+ Map()
+ );
+}
+
+module.exports = listFilters;
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..167587a
--- /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..a241c23
--- /dev/null
+++ b/packages/gitbook/src/plugins/resolveVersion.js
@@ -0,0 +1,70 @@
+const npm = require('npm');
+const semver = require('semver');
+const { Map } = 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 = 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..422a24d
--- /dev/null
+++ b/packages/gitbook/src/plugins/toNames.js
@@ -0,0 +1,16 @@
+
+/**
+ * Return list of plugin names. This method is only 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..82a2507
--- /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..cc9ac7b
--- /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;