feat: refine plugin API

This commit is contained in:
ULIVZ
2018-08-15 22:34:08 +08:00
parent 8b0adcb97e
commit 9a50862c26
28 changed files with 326 additions and 231 deletions
+1 -1
View File
@@ -1 +1 @@
module.exports = '@org/vuepress-plugin-a'
module.exports = {}
+1 -1
View File
@@ -1 +1 @@
module.exports = '@org/vuepress-plugin-b'
module.exports = {}
+1
View File
@@ -0,0 +1 @@
module.exports = {}
+1 -1
View File
@@ -1 +1 @@
module.exports = 'vuepress-plugin-a'
module.exports = {}
+1 -1
View File
@@ -1 +1 @@
module.exports = 'vuepress-plugin-b'
module.exports = {}
@@ -1,46 +1,46 @@
// TODO change to ES6 import
// https://github.com/facebook/jest/issues/6835
const Tapable = require('../../lib/plugin-api/core/Tapable')
const Option = require('../../lib/plugin-api/Option')
describe('Tapable', () => {
test('shoould tapable record the key', () => {
const tapable = new Tapable('option')
expect(tapable.key).toBe('option')
describe('Option', () => {
test('shoould option record the key', () => {
const option = new Option('option')
expect(option.key).toBe('option')
})
test('should \'tap\' work', () => {
const tapable = new Tapable('option')
tapable.tap('plugin-a', 'a')
tapable.tap('plugin-b', 'b')
expect(tapable.items).toEqual([
const option = new Option('option')
option.tap('plugin-a', 'a')
option.tap('plugin-b', 'b')
expect(option.items).toEqual([
{ value: 'a', name: 'plugin-a' },
{ value: 'b', name: 'plugin-b' }
])
expect(tapable.values).toEqual(['a', 'b'])
expect(option.values).toEqual(['a', 'b'])
})
test('should \'tap\' resolve array value', () => {
const tapable = new Tapable('option')
tapable.tap('plugin-a', ['a-1', 'a-2'])
tapable.tap('plugin-b', 'b')
expect(tapable.items).toEqual([
const option = new Option('option')
option.tap('plugin-a', ['a-1', 'a-2'])
option.tap('plugin-b', 'b')
expect(option.items).toEqual([
{ value: 'a-1', name: 'plugin-a' },
{ value: 'a-2', name: 'plugin-a' },
{ value: 'b', name: 'plugin-b' }
])
expect(tapable.values).toEqual(['a-1', 'a-2', 'b'])
expect(option.values).toEqual(['a-1', 'a-2', 'b'])
})
test('should \'run\' work', async () => {
const tapable = new Tapable('option')
const option = new Option('option')
const handler1 = jest.fn()
const handler2 = jest.fn()
tapable.tap('plugin-a', handler1)
tapable.tap('plugin-b', handler2)
option.tap('plugin-a', handler1)
option.tap('plugin-b', handler2)
await tapable.run(1, 2)
await option.run(1, 2)
expect(handler1.mock.calls).toHaveLength(1)
expect(handler2.mock.calls).toHaveLength(1)
expect(handler1.mock.calls[0][0]).toBe(1)
@@ -50,14 +50,14 @@ describe('Tapable', () => {
})
test('should \'parallelRun\' work', async () => {
const tapable = new Tapable('option')
const option = new Option('option')
const handler1 = jest.fn()
const handler2 = jest.fn()
tapable.tap('plugin-a', handler1)
tapable.tap('plugin-b', handler2)
option.tap('plugin-a', handler1)
option.tap('plugin-b', handler2)
await tapable.parallelRun(1, 2)
await option.parallelRun(1, 2)
expect(handler1.mock.calls).toHaveLength(1)
expect(handler2.mock.calls).toHaveLength(1)
expect(handler1.mock.calls[0][0]).toBe(1)
@@ -1,42 +1,15 @@
jest.mock('vuepress-plugin-a')
jest.mock('@org/vuepress-plugin-a')
const {
resolvePlugin,
resolveScopePackage
} = require('../../lib/plugin-api/util')
const Plugin = require('../../lib/plugin-api/index')
const { PLUGIN_OPTION_MAP } = require('../../lib/plugin-api/constants')
// const Plugin = require('../../lib/plugin-api/index')
describe('resolvePlugin', () => {
test('should resolve scope packages correctly', () => {
const pkg1 = resolveScopePackage('@vuepress/plugin-a')
expect(pkg1.org).toBe('vuepress')
expect(pkg1.name).toBe('plugin-a')
const pkg2 = resolveScopePackage('vuepress/plugin-a')
expect(pkg2).toBe(null)
const pkg3 = resolveScopePackage('vuepress-plugin-a')
expect(pkg3).toBe(null)
})
test('shoould return raw when function or object is given', () => {
const plugin1 = () => {}
const plugin2 = {}
expect(resolvePlugin(plugin1)).toBe(plugin1)
expect(resolvePlugin(plugin2)).toBe(plugin2)
})
// https://jestjs.io/docs/en/manual-mocks#mocking-node-modules
test('shoould resolve fullname correctly', () => {
expect(resolvePlugin('vuepress-plugin-a')).toBe('vuepress-plugin-a')
expect(resolvePlugin('@org/vuepress-plugin-a')).toBe('@org/vuepress-plugin-a')
})
// https://jestjs.io/docs/en/manual-mocks#mocking-node-modules
test('shoould resolve shortcut correctly', () => {
expect(resolvePlugin('a')).toBe('vuepress-plugin-a')
expect(resolvePlugin('@org/a')).toBe('@org/vuepress-plugin-a')
// special shortcut for vuepress
expect(resolvePlugin('@vuepress/a')).toBe('@vuepress/plugin-a')
describe('Plugin', () => {
test('should resolve scope packages correctly.', () => {
const plugin = new Plugin()
const readyHandler = () => {}
plugin.registerOption(PLUGIN_OPTION_MAP.READY.key, readyHandler)
expect(plugin.options.ready.values).toHaveLength(1)
expect(plugin.options.ready.values[0]).toBe(readyHandler)
})
})
@@ -0,0 +1,105 @@
jest.mock('vuepress-plugin-a')
jest.mock('@org/vuepress-plugin-a')
const {
resolvePlugin,
hydratePlugin,
resolveScopePackage
} = require('../../lib/plugin-api/util')
function resolveMockModule (name) {
return require(`../../../../../__mocks__/${name}`)
}
// const Plugin = require('../../lib/plugin-api/index')
describe('resolvePlugin', () => {
test('should resolve scope packages correctly.', () => {
const pkg1 = resolveScopePackage('@vuepress/plugin-a')
expect(pkg1.org).toBe('vuepress')
expect(pkg1.name).toBe('plugin-a')
const pkg2 = resolveScopePackage('vuepress/plugin-a')
expect(pkg2).toBe(null)
const pkg3 = resolveScopePackage('vuepress-plugin-a')
expect(pkg3).toBe(null)
})
test('shoould resolve local plugin as expected.', () => {
const plugin1 = () => {}
const plugin2 = {}
expect(resolvePlugin(plugin1)).toEqual({ name: 'plugin1', shortcut: 'plugin1', config: plugin1 })
expect(resolvePlugin(plugin2)).toEqual({ name: 'anonymous-1', shortcut: 'anonymous-1', config: plugin2 })
})
test('shoould resolve fullname usage correctly.', () => {
let plugin = resolvePlugin('vuepress-plugin-a')
expect(plugin.name).toBe('vuepress-plugin-a')
expect(plugin.shortcut).toBe('a')
expect(plugin.config).toBe(resolveMockModule('vuepress-plugin-a'))
plugin = resolvePlugin('@org/vuepress-plugin-a')
expect(plugin.name).toBe('@org/vuepress-plugin-a')
expect(plugin.shortcut).toBe('@org/a')
expect(plugin.config).toBe(resolveMockModule('@org/vuepress-plugin-a'))
})
test('shoould resolve shortcut usage correctly.', () => {
// normal package
let plugin = resolvePlugin('a')
expect(plugin.name).toBe('vuepress-plugin-a')
expect(plugin.shortcut).toBe('a')
expect(plugin.config).toBe(resolveMockModule('vuepress-plugin-a'))
// scope packages
plugin = resolvePlugin('@org/a')
expect(plugin.name).toBe('@org/vuepress-plugin-a')
expect(plugin.shortcut).toBe('@org/a')
expect(plugin.config).toBe(resolveMockModule('@org/vuepress-plugin-a'))
// special case for @vuepress package
plugin = resolvePlugin('@vuepress/a')
expect(plugin.name).toBe('@vuepress/plugin-a')
expect(plugin.shortcut).toBe('@vuepress/a')
expect(plugin.config).toBe(resolveMockModule('@vuepress/plugin-a'))
})
test('shoould return null when plugin cannot be resolved.', () => {
expect(resolvePlugin('c')).toEqual({ name: 'c', shortcut: 'c', config: null })
})
})
describe('hydratePlugin', () => {
test('shoould hydrate plugin correctly', () => {
const plugin = { name: 'a', shortcut: 'a', config: { enhanceAppFiles: 'file' }}
const hydratedPlugin = hydratePlugin(plugin, {}, {})
expect(hydratedPlugin.name).toBe('a')
expect(hydratedPlugin.shortcut).toBe('a')
expect(hydratedPlugin.enabled).toBe(true)
expect(hydratedPlugin.enhanceAppFiles).toBe('file')
})
test('shoould set \'enabled\' to false when \'pluginOptions\' is set to false.', () => {
const plugin = { name: 'a', shortcut: 'a', config: {}}
const hydratedPlugin = hydratePlugin(plugin, false, {})
expect(hydratedPlugin.name).toBe('a')
expect(hydratedPlugin.shortcut).toBe('a')
expect(hydratedPlugin.enabled).toBe(false)
})
test('shoould hydrate functional plugin correctly.', () => {
const config = jest.fn(() => ({ enhanceAppFiles: 'file' }))
const plugin = { name: 'a', shortcut: 'a', config }
const pluginOptions = {}
const pluginContext = {}
const hydratedPlugin = hydratePlugin(plugin, pluginOptions, pluginContext)
expect(hydratedPlugin.name).toBe('a')
expect(hydratedPlugin.shortcut).toBe('a')
expect(hydratedPlugin.enabled).toBe(true)
expect(hydratedPlugin.enhanceAppFiles).toBe('file')
expect(config.mock.calls).toHaveLength(1)
expect(config.mock.calls[0][0]).toBe(pluginOptions)
expect(Object.getPrototypeOf(config.mock.calls[0][1])).toBe(pluginContext)
})
})
+1 -1
View File
@@ -94,7 +94,7 @@ module.exports = async function build (sourceDir, cliOptions = {}) {
)
}
await options.plugin.hooks.generated.run()
await options.plugin.options.generated.run()
// DONE.
const relativeDir = path.relative(process.cwd(), outDir)
+1 -1
View File
@@ -24,7 +24,7 @@ module.exports = async function dev (sourceDir, cliOptions = {}) {
// setup watchers to update options and dynamically generated files
const update = () => {
options.plugin.hooks.updated.run()
options.plugin.options.updated.run()
prepare(sourceDir).catch(err => {
console.error(logger.error(chalk.red(err.stack), false))
})
@@ -1,4 +1,4 @@
module.exports = class Tapable {
module.exports = class Option {
constructor (key) {
this.key = key
this.items = [] // Array<{ value: T, name: string }>
@@ -1,38 +1,28 @@
const READY = 'ready'
const COMPILED = 'compiled'
const UPDATED = 'updated'
const GENERATED = 'generated'
const CHAIN_WEBPACK = 'chainWebpack'
const ENHANCE_DEV_SERVER = 'enhanceDevServer'
const ENHANCE_APP_FILES = 'enhanceAppFiles'
const OUT_FILES = 'outFiles'
const EXTEND_PAGE_DATA = 'extendPageData'
const EXTEND_MARKDOWN = 'extendMarkdown'
const CLIENT_DYNAMIC_MODULES = 'clientDynamicModules'
const CLIENT_ROOT_MIXIN = 'clientRootMixin'
const ADDITIONAL_PAGES = 'additionalPages'
const GLOBAL_UI_COMPONENTS = 'globalUIComponents'
const HOOK = {
READY,
COMPILED,
UPDATED,
GENERATED
const PLUGIN_OPTION_META_MAP = {
// hooks
READY: { name: 'ready', types: [Function] },
COMPILED: { name: 'compiled', types: [Function] },
UPDATED: { name: 'updated', types: [Function] },
GENERATED: { name: 'generated', types: [Function] },
// options
CHAIN_WEBPACK: { name: 'chainWebpack', types: [Function] },
ENHANCE_DEV_SERVER: { name: 'enhanceDevServer', types: [Function] },
ENHANCE_APP_FILES: { name: 'enhanceAppFiles', types: [Array, Function] },
OUT_FILES: { name: 'outFiles', types: [Object] },
EXTEND_PAGE_DATA: { name: 'extendPageData', types: [Function] },
EXTEND_MARKDOWN: { name: 'extendMarkdown', types: [Function] },
CLIENT_DYNAMIC_MODULES: { name: 'clientDynamicModules', types: [Function] },
CLIENT_ROOT_MIXIN: { name: 'clientRootMixin', types: [String] },
ADDITIONAL_PAGES: { name: 'additionalPages', types: [Function, Array] },
GLOBAL_UI_COMPONENTS: { name: 'globalUIComponents', types: [String, Array] }
}
const OPTION = {
CHAIN_WEBPACK,
ENHANCE_DEV_SERVER,
ENHANCE_APP_FILES,
OUT_FILES,
EXTEND_PAGE_DATA,
EXTEND_MARKDOWN,
CLIENT_DYNAMIC_MODULES,
CLIENT_ROOT_MIXIN,
ADDITIONAL_PAGES,
GLOBAL_UI_COMPONENTS
}
const PLUGIN_OPTION_MAP = {}
Object.keys(PLUGIN_OPTION_META_MAP).forEach(key => {
PLUGIN_OPTION_MAP[key] = Object.assign({ key }, PLUGIN_OPTION_META_MAP[key])
})
exports.HOOK = HOOK
exports.OPTION = OPTION
const OPTION_NAMES = Object.keys(PLUGIN_OPTION_META_MAP).map(key => PLUGIN_OPTION_META_MAP[key].name)
exports.PLUGIN_OPTION_MAP = PLUGIN_OPTION_MAP
exports.OPTION_NAMES = OPTION_NAMES
@@ -1,3 +0,0 @@
const Tapable = require('./Tapable')
module.exports = class Hook extends Tapable {}
+40 -72
View File
@@ -1,33 +1,24 @@
const chalk = require('chalk')
const Hook = require('./core/Hook')
const instantiateAPI = require('./option/instantiateOption')
const logger = require('../util/logger')
const { resolvePlugin, inferPluginName } = require('./util')
const instantiateOption = require('./option/instantiateOption')
const { resolvePlugin, hydratePlugin, normalizePluginsConfig } = require('./util')
const { assertTypes } = require('../util/shared')
const { HOOK, OPTION } = require('./constants')
const { PLUGIN_OPTION_MAP } = require('./constants')
module.exports = class Plugin {
constructor (context) {
this.hooks = {}
this.options = {}
this._pluginContext = context
this.extendHooks(Object.values(HOOK))
this.extendOptions(Object.values(OPTION))
this.initializeOptions(PLUGIN_OPTION_MAP)
}
use (pluginRaw, pluginOptions) {
use (pluginRaw, pluginOptions = {}) {
let plugin = resolvePlugin(pluginRaw)
if (typeof plugin === 'function') {
// 'Object.create' here is to give each plugin a separate context,
// but also own the inheritance context.
plugin = plugin(pluginOptions, Object.create(this._pluginContext))
if (!plugin.config) {
console.warn(`[vuepress] cannot resolve plugin "${pluginRaw}"`)
return
}
plugin = Object.assign({
enabled: true,
name: inferPluginName(pluginRaw, plugin)
}, plugin)
plugin = hydratePlugin(plugin, pluginOptions, this._pluginContext)
if (plugin.enabled) {
this.applyPlugin(plugin)
} else {
@@ -36,60 +27,39 @@ module.exports = class Plugin {
return this
}
useByConfigs (pluginConfigs) {
if (!Array.isArray(pluginConfigs)) {
pluginConfigs = []
}
pluginConfigs.forEach(pluginConfigs => {
pluginConfigs = Array.isArray(pluginConfigs)
? pluginConfigs
: [pluginConfigs]
const [pluginRaw, pluginOptions] = pluginConfigs
useByPluginsConfig (pluginsConfig) {
pluginsConfig = normalizePluginsConfig(pluginsConfig)
pluginsConfig.forEach(([pluginRaw, pluginOptions]) => {
this.use(pluginRaw, pluginOptions)
})
return this
}
extendHooks (hooks) {
hooks.forEach(hook => {
this.hooks[hook] = new Hook(hook)
initializeOptions () {
Object.keys(PLUGIN_OPTION_MAP).forEach(key => {
const option = PLUGIN_OPTION_MAP[key]
this.options[option.name] = instantiateOption(option.name)
})
}
extendOptions (options) {
options.forEach(api => {
this.options[api] = instantiateAPI(api)
})
}
registerHook (name, hook, pluginName, types) {
const { valid, warnMsg } = assertTypes(hook, types)
registerOption (key, value, pluginName) {
const option = PLUGIN_OPTION_MAP[key]
const types = option.types
const { valid, warnMsg } = assertTypes(value, types)
if (valid) {
this.hooks[name].tap(pluginName, hook)
} else if (hook !== undefined) {
this.options[option.name].tap(pluginName, value)
} else if (value !== undefined) {
logger.warn(
`${chalk.gray(`[vuepress-plugin-${pluginName}]`)} ` +
`Invalid value for "hook" ${chalk.cyan(name)}: ${warnMsg}`
)
}
return this
}
registerOption (name, api, pluginName, types) {
const { valid, warnMsg } = assertTypes(api, types)
if (valid) {
this.options[name].tap(pluginName, api)
} else if (api !== undefined) {
logger.warn(
`${chalk.gray(`[vuepress-plugin-${pluginName}]`)} ` +
`Invalid value for "option" ${chalk.cyan(name)}: ${warnMsg}`
`Invalid value for "option" ${chalk.cyan(option.name)}: ${warnMsg}`
)
}
return this
}
applyPlugin ({
name,
name: pluginName,
shortcut,
chainWebpack,
enhanceDevServer,
extendMarkdown,
@@ -105,24 +75,22 @@ module.exports = class Plugin {
additionalPages,
globalUIComponents
}) {
logger.tip(`\nApply plugin ${chalk.gray(name)}...`)
logger.tip(`\nApply plugin ${chalk.gray(pluginName)}...`)
this
.registerHook(HOOK.READY, ready, name, [Function])
.registerHook(HOOK.COMPILED, compiled, name, [Function])
.registerHook(HOOK.UPDATED, updated, name, [Function])
.registerHook(HOOK.GENERATED, generated, name, [Function])
this
.registerOption(OPTION.CHAIN_WEBPACK, chainWebpack, name, [Function])
.registerOption(OPTION.ENHANCE_DEV_SERVER, enhanceDevServer, name, [Function])
.registerOption(OPTION.EXTEND_MARKDOWN, extendMarkdown, name, [Function])
.registerOption(OPTION.EXTEND_PAGE_DATA, extendPageData, name, [Function])
.registerOption(OPTION.ENHANCE_APP_FILES, enhanceAppFiles, name, [Array, Function])
.registerOption(OPTION.OUT_FILES, outFiles, name, [Object])
.registerOption(OPTION.CLIENT_DYNAMIC_MODULES, clientDynamicModules, name, [Function])
.registerOption(OPTION.CLIENT_ROOT_MIXIN, clientRootMixin, name, [String])
.registerOption(OPTION.ADDITIONAL_PAGES, additionalPages, name, [Function, Array])
.registerOption(OPTION.GLOBAL_UI_COMPONENTS, globalUIComponents, name, [String, Array])
.registerOption(PLUGIN_OPTION_MAP.READY.key, ready, pluginName)
.registerOption(PLUGIN_OPTION_MAP.COMPILED.key, compiled, pluginName)
.registerOption(PLUGIN_OPTION_MAP.UPDATED.key, updated, pluginName)
.registerOption(PLUGIN_OPTION_MAP.GENERATED.key, generated, pluginName)
.registerOption(PLUGIN_OPTION_MAP.CHAIN_WEBPACK.key, chainWebpack, pluginName)
.registerOption(PLUGIN_OPTION_MAP.ENHANCE_DEV_SERVER.key, enhanceDevServer, pluginName)
.registerOption(PLUGIN_OPTION_MAP.EXTEND_MARKDOWN.key, extendMarkdown, pluginName)
.registerOption(PLUGIN_OPTION_MAP.EXTEND_PAGE_DATA.key, extendPageData, pluginName)
.registerOption(PLUGIN_OPTION_MAP.ENHANCE_APP_FILES.key, enhanceAppFiles, pluginName)
.registerOption(PLUGIN_OPTION_MAP.OUT_FILES.key, outFiles, pluginName)
.registerOption(PLUGIN_OPTION_MAP.CLIENT_DYNAMIC_MODULES.key, clientDynamicModules, pluginName)
.registerOption(PLUGIN_OPTION_MAP.CLIENT_ROOT_MIXIN.key, clientRootMixin, pluginName)
.registerOption(PLUGIN_OPTION_MAP.ADDITIONAL_PAGES.key, additionalPages, pluginName)
.registerOption(PLUGIN_OPTION_MAP.GLOBAL_UI_COMPONENTS.key, globalUIComponents, pluginName)
}
}
@@ -1,6 +1,6 @@
const Tapable = require('../core/Tapable')
const Option = require('../Option')
module.exports = class AdditionalPagesOption extends Tapable {
module.exports = class AdditionalPagesOption extends Option {
tap (pluginName, value) {
if (typeof value === 'function') {
value = value()
@@ -1,7 +1,7 @@
const Tapable = require('../core/Tapable')
const Option = require('../Option')
const { writeTemp } = require('../../prepare/util')
module.exports = class ClientDynamicModulesOption extends Tapable {
module.exports = class ClientDynamicModulesOption extends Option {
async run () {
for (const item of this.items) {
const { value: fn, name: pluginName } = item
@@ -1,11 +1,11 @@
const fs = require('fs-extra')
const chalk = require('chalk')
const Tapable = require('../core/Tapable')
const Option = require('../Option')
const { writeTemp } = require('../../prepare/util')
const { pathsToModuleCode } = require('../../prepare/codegen')
const logger = require('../../util/logger')
module.exports = class EnhanceAppFilesOption extends Tapable {
module.exports = class EnhanceAppFilesOption extends Option {
/**
* In fact, we can quickly implement support for function parameters
* by overriding 'tap', but 'tap' will be executed immediately
@@ -1,6 +1,6 @@
const Tapable = require('../core/Tapable')
const Option = require('../Option')
module.exports = class ExtendPageDataOption extends Tapable {
module.exports = class ExtendPageDataOption extends Option {
async run (args) {
const { data } = args
for (const fn of this.values) {
@@ -1,7 +1,7 @@
const Tapable = require('../core/Tapable')
const Option = require('../Option')
const { writeTemp } = require('../../prepare/util')
module.exports = class GlobalUIComponentsOption extends Tapable {
module.exports = class GlobalUIComponentsOption extends Option {
async run () {
writeTemp(
`dynamic-modules/global-ui.js`,
@@ -3,26 +3,26 @@ const ExtendPageDataOption = require('./ExtendPageDataOption')
const ClientDynamicModulesOption = require('./ClientDynamicModulesOption')
const AdditionalPagesOption = require('./AdditionalPagesOption')
const GlobalUIComponentsOption = require('./GlobalUIComponentsOption')
const Tapable = require('../core/Tapable')
const { OPTION } = require('../constants')
const Option = require('../Option')
const { PLUGIN_OPTION_MAP } = require('../constants')
module.exports = function instantiateOption (name) {
switch (name) {
case OPTION.ENHANCE_APP_FILES:
case PLUGIN_OPTION_MAP.ENHANCE_APP_FILES.name:
return new EnhanceAppFilesOption(name)
case OPTION.EXTEND_PAGE_DATA:
case PLUGIN_OPTION_MAP.EXTEND_PAGE_DATA.name:
return new ExtendPageDataOption(name)
case OPTION.CLIENT_DYNAMIC_MODULES:
case PLUGIN_OPTION_MAP.CLIENT_DYNAMIC_MODULES.name:
return new ClientDynamicModulesOption(name)
case OPTION.ADDITIONAL_PAGES:
case PLUGIN_OPTION_MAP.ADDITIONAL_PAGES.name:
return new AdditionalPagesOption(name)
case OPTION.GLOBAL_UI_COMPONENTS:
case PLUGIN_OPTION_MAP.GLOBAL_UI_COMPONENTS.name:
return new GlobalUIComponentsOption(name)
default: return new Tapable(name)
default: return new Option(name)
}
}
+82 -21
View File
@@ -1,7 +1,10 @@
const logger = require('../util/logger')
const chalk = require('chalk')
const { isDebug } = require('../util/logger')
const logger = require('../util/logger')
const { assertTypes } = require('../util/shared')
const SCOPE_PACKAGE_RE = /^@(.*)\/(.*)/
let anonymousPluginIdx = 0
exports.resolveScopePackage = function (name) {
if (SCOPE_PACKAGE_RE.test(name)) {
@@ -13,44 +16,102 @@ exports.resolveScopePackage = function (name) {
return null
}
/**
* Resolve plugin config, name, and shortcut.
* @param pluginRaw
* @returns {{config: Function|Object, name: string, shortcut: string}}
*/
exports.resolvePlugin = function (pluginRaw) {
let name
let config
let shortcut
if (typeof pluginRaw === 'function' || typeof pluginRaw === 'object') {
return pluginRaw
}
if (typeof pluginRaw === 'string') {
config = pluginRaw
name = shortcut = pluginRaw.name || `anonymous-${++anonymousPluginIdx}`
} else if (typeof pluginRaw === 'string') {
try {
return require(pluginRaw.startsWith('vuepress-plugin-') ? pluginRaw : `vuepress-plugin-${pluginRaw}`)
shortcut = pluginRaw.startsWith('vuepress-plugin-') ? pluginRaw.slice(16) : pluginRaw
name = `vuepress-plugin-${shortcut}`
config = require(name)
} catch (err) {
const pkg = exports.resolveScopePackage(pluginRaw)
try {
if (pkg) {
if (pkg.org === 'vuepress') {
return require(pkg.name.startsWith('plugin-') ? pluginRaw : `@vuepress/plugin-${pkg.name}`)
shortcut = pkg.name.startsWith('plugin-') ? pkg.name.slice(7) : pkg.name
name = `@vuepress/plugin-${shortcut}`
} else {
return require(pkg.name.startsWith('vuepress-plugin-') ? pluginRaw : `@${pkg.org}/vuepress-plugin-${pkg.name}`)
shortcut = pkg.name.startsWith('vuepress-plugin-') ? pkg.name.slice(16) : pkg.name
name = `@${pkg.org}/vuepress-plugin-${shortcut}`
}
shortcut = `@${pkg.org}/${shortcut}`
config = require(name)
} else {
throw new Error(`[vuepress] Cannot resolve ${pluginRaw}.`)
throw new Error(`[vuepress] Invalid plugin usage ${pluginRaw}.`)
}
} catch (err2) {
console.error(chalk.red(logger.error(`\n[vuepress] Cannot resolve plugin: ${pluginRaw}\n`, false)))
throw new Error(err2)
if (isDebug) {
console.error(err2)
}
name = shortcut = pluginRaw
config = null
}
}
}
logger.warn(`\n[vuepress] Invalid plugin usage: ${chalk.yellow(pluginRaw)}\n`)
return { config, name, shortcut }
}
exports.inferPluginName = function (pluginRaw, pluginConfig) {
if (pluginConfig.name) {
return pluginConfig.name
/**
* Hydrates your plugin config, options and context.
* @param {Function | Object} config
* @param {String} name
* @param {String} hortcut
* @param {Object} pluginOptions
* @param {Object} pluginContext
*/
exports.hydratePlugin = function ({ config, name, shortcut }, pluginOptions, pluginContext) {
const { valid, warnMsg } = assertTypes(pluginOptions, [Object, Boolean])
if (!valid) {
logger.warn(
`[${chalk.gray(shortcut)}] ` +
`Invalid value for "pluginOptions" ${chalk.cyan(name)}: ${warnMsg}`
)
pluginOptions = {}
}
if (typeof pluginRaw === 'string') {
if (pluginRaw.startsWith('vuepress-plugin-')) {
return pluginRaw.slice(16)
}
return pluginRaw
let enabled = true
if (typeof pluginOptions === 'boolean') {
enabled = pluginOptions
pluginOptions = {}
}
// ensure each plugin have a unique name.
return Date.now().toString(16)
if (typeof config === 'function') {
// 'Object.create' here is to give each plugin a separate context,
// but also own the inheritance context.
config = config(pluginOptions, Object.create(pluginContext))
}
return Object.assign(config, { name, shortcut, enabled })
}
/**
* Normalize plugins config in `.vuepress/config.js`
* @param pluginsConfig
*/
exports.normalizePluginsConfig = function (pluginsConfig) {
const { valid, warnMsg } = assertTypes(pluginsConfig, [Object, Array])
if (!valid) {
logger.warn(
`[${chalk.gray('config')}] ` +
`Invalid value for "plugin" value ${chalk.cyan(name)}: ${warnMsg}`
)
pluginsConfig = []
}
if (Array.isArray(pluginsConfig)) {
pluginsConfig = pluginsConfig.map(item => {
return Array.isArray(item) ? item : [item]
})
} else if (typeof pluginsConfig === 'object') {
pluginsConfig = Object.keys(pluginsConfig).map(item => {
return [item, pluginsConfig[item]]
})
}
return pluginsConfig
}
+6 -5
View File
@@ -21,6 +21,7 @@ module.exports = async function prepare ({
// 2. resolve plugin
const plugin = resolvePlugin(options)
options.plugin = plugin
const pluginOptions = plugin.options
// 3. resolve siteData
// SiteData must be resolved after the plugin initialization
@@ -28,12 +29,12 @@ module.exports = async function prepare ({
options.siteData = await resolveSiteData(options)
Object.freeze(options)
await plugin.hooks.ready.run()
await pluginOptions.ready.run()
// 4. apply plugin options to extend markdown.
plugin.options.extendMarkdown.run(markdown)
plugin.options.clientDynamicModules.run()
plugin.options.globalUIComponents.run()
pluginOptions.extendMarkdown.run(markdown)
pluginOptions.clientDynamicModules.run()
pluginOptions.globalUIComponents.run()
// 5. generate routes code
const routesCode = await genRoutesFile(options)
@@ -60,7 +61,7 @@ module.exports = async function prepare ({
)
}
await plugin.options.enhanceAppFiles.run()
await pluginOptions.enhanceAppFiles.run()
return options
}
@@ -13,9 +13,9 @@ module.exports = function (options) {
plugin
// user plugin
.useByConfigs(cliPlugins)
.useByConfigs(siteConfig.plugins)
.useByConfigs(themePlugins)
.useByPluginsConfig(cliPlugins)
.useByPluginsConfig(siteConfig.plugins)
.useByPluginsConfig(themePlugins)
// built-in plugins
.use(enhanceAppPlugin)
.use(registerGlobalComponentsPlugin, {
@@ -55,3 +55,4 @@ logger.debug = function (msg) {
module.exports = logger
module.exports.getLoggerFn = getLoggerFn
module.exports.isDebug = isDebug
+1 -1
View File
@@ -15,7 +15,7 @@ module.exports = (options, context) => ({
// globalComponents: ['BackTIoTop'],
// Usually used to construct a global UI, e.g. back to top.
globalUIComponents: ['BackToTop'],
// globalUIComponents: ['BackToTop'],
clientRootMixin: path.resolve(__dirname, 'mixin.js'),
+2 -4
View File
@@ -1,7 +1,5 @@
const path = require('path')
const themePlugin = require('./plugin')
const activeHeaderLinksPlugin = require('@vuepress/plugin-active-header-links')
const googleAnalyticsPlugin = require('@vuepress/plugin-google-analytics')
// Theme API.
module.exports = {
@@ -9,7 +7,7 @@ module.exports = {
notFound: path.resolve(__dirname, 'src/NotFound.vue'),
plugins: [
themePlugin,
activeHeaderLinksPlugin,
googleAnalyticsPlugin
'@vuepress/active-header-links',
'@vuepress/google-analytics'
]
}
+1 -1
View File
@@ -24,7 +24,7 @@ module.exports = {
['meta', { name: 'msapplication-TileColor', content: '#000000' }]
],
serviceWorker: true,
plugins: ['back-to-top'],
plugins: ['@vuepress/back-to-top'],
themeConfig: {
repo: 'vuejs/vuepress',
editLinks: true,
+2 -2
View File
@@ -3,7 +3,7 @@
require('@vuepress/cli').bootstrap({
theme: 'default',
plugins: [
'test',
'i18n-ui'
'@vuepress/test',
'@vuepress/i18n-ui'
]
})