diff options
Diffstat (limited to 'packages/gitbook/src/plugins')
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; |