Skip to content

Commit f81f373

Browse files
committed
feat: refine theme API
1. Support scope package and shortcut like plugin. 2. Support relative path for 'layout' and 'notFound' option.
1 parent 76b74b1 commit f81f373

File tree

12 files changed

+192
-82
lines changed

12 files changed

+192
-82
lines changed
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {}

__mocks__/@vuepress/theme-a/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {}

__mocks__/vuepress-theme-a/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {}

packages/@vuepress/core/lib/prepare/AppContext.js

+3-65
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const path = require('path')
22
const createMarkdown = require('../markdown/index')
33
const loadConfig = require('./loadConfig')
4+
const loadTheme = require('./loadTheme')
45
const { fs, logger, chalk, globby, sort } = require('@vuepress/shared-utils')
56

67
const Page = require('./Page')
@@ -87,7 +88,7 @@ module.exports = class AppContext {
8788
// user plugin
8889
.useByPluginsConfig(this._options.plugins)
8990
.useByPluginsConfig(this.siteConfig.plugins)
90-
.useByPluginsConfig(this.themeplugins)
91+
.useByPluginsConfig(this.themePlugins)
9192
// built-in plugins
9293
.use('@vuepress/last-updated', shouldUseLastUpdated)
9394
.use('@vuepress/register-components', {
@@ -168,70 +169,7 @@ module.exports = class AppContext {
168169
*/
169170
async resolveTheme () {
170171
const theme = this.siteConfig.theme || this._options.theme
171-
const requireResolve = (target) => {
172-
return require.resolve(target, {
173-
paths: [
174-
path.resolve(__dirname, '../../node_modules'),
175-
path.resolve(this.sourceDir)
176-
]
177-
})
178-
}
179-
180-
// resolve theme
181-
const localThemePath = path.resolve(this.vuepressDir, 'theme')
182-
const useLocalTheme = await fs.exists(localThemePath)
183-
184-
let themePath = null
185-
let themeLayoutPath = null
186-
let themeNotFoundPath = null
187-
let themeIndexFile = null
188-
let themePlugins = []
189-
190-
if (useLocalTheme) {
191-
logger.tip(`\nApply theme located at ${localThemePath}...`)
192-
193-
// use local custom theme
194-
themePath = localThemePath
195-
themeLayoutPath = path.resolve(localThemePath, 'Layout.vue')
196-
themeNotFoundPath = path.resolve(localThemePath, 'NotFound.vue')
197-
if (!fs.existsSync(themeLayoutPath)) {
198-
throw new Error(`[vuepress] Cannot resolve Layout.vue file in .vuepress/theme.`)
199-
}
200-
if (!fs.existsSync(themeNotFoundPath)) {
201-
themeNotFoundPath = path.resolve(__dirname, '../app/components/NotFound.vue')
202-
}
203-
} else if (theme) {
204-
// use external theme
205-
try {
206-
// backward-compatible 0.x.x.
207-
themeLayoutPath = requireResolve(`vuepress-theme-${theme}/Layout.vue`)
208-
themePath = path.dirname(themeLayoutPath)
209-
themeNotFoundPath = path.resolve(themeLayoutPath, 'NotFound.vue')
210-
} catch (e) {
211-
try {
212-
themeIndexFile = requireResolve(`vuepress-theme-${theme}/index.js`)
213-
} catch (e) {
214-
try {
215-
themeIndexFile = requireResolve(`@vuepress/theme-${theme}`)
216-
themePath = path.dirname(themeIndexFile)
217-
themeIndexFile = require(themeIndexFile)
218-
themeLayoutPath = themeIndexFile.layout
219-
themeNotFoundPath = themeIndexFile.notFound
220-
themePlugins = themeIndexFile.plugins
221-
} catch (e) {
222-
throw new Error(`[vuepress] Failed to load custom theme "${theme}". File vuepress-theme-${theme}/Layout.vue does not exist.`)
223-
}
224-
}
225-
}
226-
logger.tip(`\nApply theme ${chalk.gray(theme)}`)
227-
} else {
228-
throw new Error(`[vuepress] You must specify a theme, or create a local custom theme. \n For more details, refer to https://vuepress.vuejs.org/guide/custom-themes.html#custom-themes. \n`)
229-
}
230-
231-
this.themePath = themePath
232-
this.themeLayoutPath = themeLayoutPath
233-
this.themeNotFoundPath = themeNotFoundPath
234-
this.themeplugins = themePlugins
172+
Object.assign(this, (await loadTheme(theme, this.sourceDir, this.vuepressDir)))
235173
}
236174

237175
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const path = require('path')
2+
const fs = require('fs')
3+
const {
4+
logger, chalk,
5+
datatypes: { isString },
6+
shortcutPackageResolver: { resolveTheme }
7+
} = require('@vuepress/shared-utils')
8+
9+
module.exports = async function loadTheme (theme, sourceDir, vuepressDir) {
10+
// resolve theme
11+
const localThemePath = path.resolve(vuepressDir, 'theme')
12+
const useLocalTheme =
13+
(await fs.exists(localThemePath)) && ((await fs.readdir(localThemePath)).length > 0)
14+
15+
let themePath = null // Mandatory
16+
let themeLayoutPath = null // Mandatory
17+
let themeNotFoundPath = null // Optional
18+
let themeIndexFile = null // Optional
19+
let themePlugins = [] // Optional
20+
let themeName
21+
let themeShortcut
22+
23+
if (useLocalTheme) {
24+
// use local custom theme
25+
themePath = localThemePath
26+
logger.tip(`\nApply theme located at ${themePath}...`)
27+
} else if (isString(theme)) {
28+
// use external theme
29+
const { module: modulePath, name, shortcut } = resolveTheme(theme, sourceDir)
30+
if (modulePath.endsWith('.js') || modulePath.endsWith('.vue')) {
31+
themePath = path.parse(modulePath).dir
32+
}
33+
themeName = name
34+
themeShortcut = shortcut
35+
logger.tip(`\nApply theme ${chalk.gray(themeName)}`)
36+
} else {
37+
throw new Error(`[vuepress] You must specify a theme, or create a local custom theme. \n For more details, refer to https://vuepress.vuejs.org/guide/custom-themes.html#custom-themes. \n`)
38+
}
39+
40+
try {
41+
themeIndexFile = require(themePath)
42+
} catch (error) {
43+
console.log(error)
44+
themeIndexFile = {}
45+
}
46+
47+
// handle theme api
48+
const { layout, notFound, plugins } = themeIndexFile
49+
themePlugins = plugins
50+
themeLayoutPath = layout
51+
? path.resolve(themePath, layout)
52+
: path.resolve(themePath, 'Layout.vue')
53+
54+
themeNotFoundPath = notFound
55+
? path.resolve(themePath, notFound)
56+
: path.resolve(themePath, 'NotFound.vue')
57+
58+
if (!fs.existsSync(themeLayoutPath)) {
59+
throw new Error(`[vuepress] Cannot resolve Layout.vue file in \n ${themeLayoutPath}`)
60+
}
61+
62+
if (!fs.existsSync(themeNotFoundPath)) {
63+
themeNotFoundPath = path.resolve(__dirname, '../app/components/NotFound.vue')
64+
}
65+
66+
return {
67+
themePath,
68+
themeLayoutPath,
69+
themeNotFoundPath,
70+
themeIndexFile,
71+
themePlugins,
72+
themeName,
73+
themeShortcut
74+
}
75+
}

packages/@vuepress/shared-utils/__test__/shortcutPackageResolver.spec.js

+13-7
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@ jest.mock('vuepress-theme-a')
66
jest.mock('@org/vuepress-theme-a')
77
jest.mock('@vuepress/theme-a')
88

9+
import path from 'path'
910
import {
1011
resolveTheme,
1112
resolvePlugin,
1213
resolveScopePackage
1314
} from '../lib/shortcutPackageResolver'
1415

16+
const MOCK_RELATIVE = '../../../../__mocks__'
17+
18+
function loadMockModule (name) {
19+
return require(`${MOCK_RELATIVE}/${name}`)
20+
}
21+
1522
function resolveMockModule (name) {
16-
return require(`../../../../__mocks__/${name}`)
23+
return path.resolve(__dirname, `${MOCK_RELATIVE}/${name}`)
1724
}
1825

1926
test('should resolve scope packages correctly.', () => {
@@ -47,36 +54,35 @@ describe('resolvePlugin', () => {
4754
})
4855

4956
test('shoould resolve fullname usage correctly.', () => {
50-
console.log(resolvePlugin())
5157
let plugin = resolvePlugin('vuepress-plugin-a')
5258
expect(plugin.name).toBe('vuepress-plugin-a')
5359
expect(plugin.shortcut).toBe('a')
54-
expect(plugin.module).toBe(resolveMockModule('vuepress-plugin-a'))
60+
expect(plugin.module).toBe(loadMockModule('vuepress-plugin-a'))
5561

5662
plugin = resolvePlugin('@org/vuepress-plugin-a')
5763
expect(plugin.name).toBe('@org/vuepress-plugin-a')
5864
expect(plugin.shortcut).toBe('@org/a')
59-
expect(plugin.module).toBe(resolveMockModule('@org/vuepress-plugin-a'))
65+
expect(plugin.module).toBe(loadMockModule('@org/vuepress-plugin-a'))
6066
})
6167

6268
test('shoould resolve shortcut usage correctly.', () => {
6369
// normal package
6470
let plugin = resolvePlugin('a')
6571
expect(plugin.name).toBe('vuepress-plugin-a')
6672
expect(plugin.shortcut).toBe('a')
67-
expect(plugin.module).toBe(resolveMockModule('vuepress-plugin-a'))
73+
expect(plugin.module).toBe(loadMockModule('vuepress-plugin-a'))
6874

6975
// scope packages
7076
plugin = resolvePlugin('@org/a')
7177
expect(plugin.name).toBe('@org/vuepress-plugin-a')
7278
expect(plugin.shortcut).toBe('@org/a')
73-
expect(plugin.module).toBe(resolveMockModule('@org/vuepress-plugin-a'))
79+
expect(plugin.module).toBe(loadMockModule('@org/vuepress-plugin-a'))
7480

7581
// special case for @vuepress package
7682
plugin = resolvePlugin('@vuepress/a')
7783
expect(plugin.name).toBe('@vuepress/plugin-a')
7884
expect(plugin.shortcut).toBe('@vuepress/a')
79-
expect(plugin.module).toBe(resolveMockModule('@vuepress/plugin-a'))
85+
expect(plugin.module).toBe(loadMockModule('@vuepress/plugin-a'))
8086
})
8187

8288
test('shoould return null when plugin cannot be resolved.', () => {
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const isDebug = process.argv.indexOf('--debug') !== -1
22
const isProduction = () => process.env.NODE_ENV === 'production'
3+
const isTest = () => process.env.NODE_ENV === 'test'
34

45
exports.isDebug = isDebug
6+
exports.isTest = isTest
57
exports.isProduction = isProduction
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Midified from https://github.com/vuejs/vue-cli/blob/dev/packages/@0vue/cli-shared-utils/lib/module.js
2+
3+
const semver = require('semver')
4+
const path = require('path')
5+
const fs = require('fs-extra')
6+
const { isTest } = require('./env')
7+
8+
function resolveFallback (request, options) {
9+
const Module = require('module')
10+
const isMain = false
11+
const fakeParent = new Module('', null)
12+
13+
const paths = []
14+
15+
for (let i = 0; i < options.paths.length; i++) {
16+
const path = options.paths[i]
17+
fakeParent.paths = Module._nodeModulePaths(path)
18+
const lookupPaths = Module._resolveLookupPaths(request, fakeParent, true)
19+
20+
if (!paths.includes(path)) paths.push(path)
21+
22+
for (let j = 0; j < lookupPaths.length; j++) {
23+
if (!paths.includes(lookupPaths[j])) paths.push(lookupPaths[j])
24+
}
25+
}
26+
27+
const filename = Module._findPath(request, paths, isMain)
28+
if (!filename) {
29+
const err = new Error(`Cannot find module '${request}'`)
30+
err.code = 'MODULE_NOT_FOUND'
31+
throw err
32+
}
33+
return filename
34+
}
35+
36+
const resolve = semver.satisfies(process.version, '>=10.0.0')
37+
? require.resolve
38+
: resolveFallback
39+
40+
exports.resolveModule = function (request, context) {
41+
let resolvedPath
42+
// TODO
43+
// Temporary workaround for jest cannot resolve module path from '__mocks__'
44+
// when using 'require.resolve'.
45+
if (isTest()) {
46+
resolvedPath = path.resolve(__dirname, '../../../../__mocks__', request)
47+
if (!fs.existsSync(`${resolvedPath}.js`) && !fs.existsSync(`${resolvedPath}/index.js`)) {
48+
throw new Error(`Cannot find module '${request}'`)
49+
}
50+
return resolvedPath
51+
}
52+
resolvedPath = resolve(request, {
53+
paths: [context || process.cwd()]
54+
})
55+
return resolvedPath
56+
}
57+
58+
exports.loadModule = function (request, context, force = false) {
59+
const resolvedPath = exports.resolveModule(request, context)
60+
if (resolvedPath) {
61+
if (force) {
62+
clearRequireCache(resolvedPath)
63+
}
64+
return require(resolvedPath)
65+
}
66+
}
67+
68+
exports.clearModule = function (request, context) {
69+
const resolvedPath = exports.resolveModule(request, context)
70+
if (resolvedPath) {
71+
clearRequireCache(resolvedPath)
72+
}
73+
}
74+
75+
function clearRequireCache (id, map = new Map()) {
76+
const module = require.cache[id]
77+
if (module) {
78+
map.set(id, true)
79+
// Clear children modules
80+
module.children.forEach(child => {
81+
if (!map.get(child.id)) clearRequireCache(child.id, map)
82+
})
83+
delete require.cache[id]
84+
}
85+
}

packages/@vuepress/shared-utils/lib/shortcutPackageResolver.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { isDebug } = require('./env')
2+
const { resolveModule, loadModule } = require('./module')
23
const {
34
isString,
45
assertTypes
@@ -15,15 +16,16 @@ const SCOPE_PACKAGE_RE = /^@(.*)\/(.*)/
1516
function shortcutPackageResolver (
1617
type = 'plugin',
1718
org = 'vuepress',
18-
allowedTypes = [String]
19+
allowedTypes = [String],
20+
load = false
1921
) {
2022
let anonymousPluginIdx = 0
2123
const NON_SCOPE_PREFIX = `${org}-${type}-`
2224
const SCOPE_PREFIX = `@${org}/${type}-`
2325
const SHORT_PREFIX_SLICE_LENGTH = type.length + 1
2426
const FULL_PREFIX_SLICE_LENGTH = SHORT_PREFIX_SLICE_LENGTH + org.length + 1
2527

26-
return function (req) {
28+
return function (req, cwd) {
2729
let _module
2830
let name
2931
let shortcut
@@ -44,7 +46,7 @@ function shortcutPackageResolver (
4446
? req.slice(FULL_PREFIX_SLICE_LENGTH)
4547
: req
4648
name = `${NON_SCOPE_PREFIX}${shortcut}`
47-
_module = require(name)
49+
_module = load ? loadModule(name, cwd) : resolveModule(name, cwd)
4850
} catch (err) {
4951
const pkg = module.exports.resolveScopePackage(req)
5052
try {
@@ -62,7 +64,7 @@ function shortcutPackageResolver (
6264
name = `@${pkg.org}/${NON_SCOPE_PREFIX}${shortcut}`
6365
}
6466
shortcut = `@${pkg.org}/${shortcut}`
65-
_module = require(name)
67+
_module = load ? loadModule(name, cwd) : resolveModule(name, cwd)
6668
} else {
6769
throw new Error(`Invalid ${type} usage ${req}.`)
6870
}
@@ -91,7 +93,7 @@ module.exports.resolveScopePackage = function resolveScopePackage (name) {
9193
return null
9294
}
9395
module.exports.resolvePlugin = shortcutPackageResolver(
94-
'plugin', 'vuepress', [String, Function, Object]
96+
'plugin', 'vuepress', [String, Function, Object], true /* load module */
9597
)
9698
module.exports.resolveTheme = shortcutPackageResolver(
9799
'theme', 'vuepress', [String]

packages/@vuepress/test-utils/prepare.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const { docsModes } = require('./meta')
55
async function prepareForTest () {
66
await Promise.all(docsModes.map(async ({ docsPath, docsTempPath }) => {
77
await fs.ensureDir(docsTempPath)
8-
await prepare(docsPath, { theme: 'default', temp: docsTempPath })
8+
await prepare(docsPath, { theme: '@vuepress/default', temp: docsTempPath })
99
}))
1010
}
1111

0 commit comments

Comments
 (0)