refactor: generate routes and template files in builder (#4883)

This commit is contained in:
Xin Du (Clark) 2019-01-30 16:54:26 +00:00 committed by GitHub
parent 3348cd6cd5
commit 4c5a59e149
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 236 additions and 204 deletions

View File

@ -5,7 +5,6 @@ import fsExtra from 'fs-extra'
import Glob from 'glob'
import hash from 'hash-sum'
import pify from 'pify'
import serialize from 'serialize-javascript'
import upath from 'upath'
import semver from 'semver'
@ -15,16 +14,11 @@ import template from 'lodash/template'
import uniq from 'lodash/uniq'
import uniqBy from 'lodash/uniqBy'
import devalue from '@nuxt/devalue'
import {
r,
wp,
wChunk,
createRoutes,
relativeTo,
waitFor,
serializeFunction,
determineGlobals,
stripWhitespace,
isString,
@ -32,7 +26,8 @@ import {
} from '@nuxt/utils'
import Ignore from './ignore'
import BuildContext from './context'
import BuildContext from './context/build'
import TemplateContext from './context/template'
const glob = pify(Glob)
@ -106,78 +101,6 @@ export default class Builder {
return new BundleBuilder(context)
}
normalizePlugins() {
return uniqBy(
this.options.plugins.map((p) => {
if (typeof p === 'string') {
p = { src: p }
}
const pluginBaseName = path.basename(p.src, path.extname(p.src)).replace(
/[^a-zA-Z?\d\s:]/g,
''
)
if (p.ssr === false) {
p.mode = 'client'
} else if (p.mode === undefined) {
p.mode = 'all'
} else if (!['client', 'server', 'all'].includes(p.mode)) {
consola.warn(`Invalid plugin mode (server/client/all): '${p.mode}'. Falling back to 'all'`)
p.mode = 'all'
}
return {
src: this.nuxt.resolver.resolveAlias(p.src),
mode: p.mode,
name: 'nuxt_plugin_' + pluginBaseName + '_' + hash(p.src)
}
}),
p => p.name
)
}
async resolveFiles(dir, cwd = this.options.srcDir) {
return this.ignore.filter(await glob(`${dir}/**/*.{${this.supportedExtensions.join(',')}}`, {
cwd,
ignore: this.options.ignore
}))
}
async resolveRelative(dir) {
const dirPrefix = new RegExp(`^${dir}/`)
return (await this.resolveFiles(dir)).map(file => ({ src: file.replace(dirPrefix, '') }))
}
resolvePlugins() {
// Check plugins exist then set alias to their real path
return Promise.all(this.plugins.map(async (p) => {
const ext = '{?(.+([^.])),/index.+([^.])}'
const pluginFiles = await glob(`${p.src}${ext}`)
if (!pluginFiles || pluginFiles.length === 0) {
throw new Error(`Plugin not found: ${p.src}`)
}
if (pluginFiles.length > 1 && !isIndexFileAndFolder(pluginFiles)) {
consola.warn({
message: `Found ${pluginFiles.length} plugins that match the configuration, suggest to specify extension:`,
additional: '\n' + pluginFiles.map(x => `- ${x}`).join('\n')
})
}
const modes = ['client', 'server']
const modePattern = new RegExp(`\\.(${modes.join('|')})\\.\\w+$`)
pluginFiles[0].replace(modePattern, (_, mode) => {
// mode in nuxt.config has higher priority
if (p.mode === 'all' && modes.includes(mode)) {
p.mode = mode
}
})
p.src = this.relativeToBuild(p.src)
}))
}
forGenerate() {
this.bundleBuilder.forGenerate()
}
@ -297,12 +220,12 @@ export default class Builder {
// Suggest dependency fixes (TODO: automate me)
if (dpendencyFixes.length) {
consola.error(
`Please install missing dependencies:\n`,
'Please install missing dependencies:\n',
'\n',
`Using yarn:\n`,
'Using yarn:\n',
`yarn add ${dpendencyFixes.join(' ')}\n`,
'\n',
`Using npm:\n`,
'Using npm:\n',
`npm i ${dpendencyFixes.join(' ')}\n`
)
throw new Error('Missing Template Dependencies')
@ -310,59 +233,82 @@ export default class Builder {
}
async generateRoutesAndFiles() {
consola.debug(`Generating nuxt files`)
consola.debug('Generating nuxt files')
// Plugins
this.plugins = Array.from(this.normalizePlugins())
// -- Templates --
let templatesFiles = Array.from(this.template.files)
const templateContext = new TemplateContext(this, this.options)
const templateVars = {
options: this.options,
extensions: this.options.extensions
.map(ext => ext.replace(/^\./, ''))
.join('|'),
messages: this.options.messages,
splitChunks: this.options.build.splitChunks,
uniqBy,
isDev: this.options.dev,
isTest: this.options.test,
debug: this.options.debug,
vue: { config: this.options.vue.config },
mode: this.options.mode,
router: this.options.router,
env: this.options.env,
head: this.options.head,
store: this.options.store,
globalName: this.options.globalName,
globals: this.globals,
css: this.options.css,
plugins: this.plugins,
appPath: './App.js',
layouts: Object.assign({}, this.options.layouts),
loading:
typeof this.options.loading === 'string'
? this.relativeToBuild(this.options.srcDir, this.options.loading)
: this.options.loading,
transition: this.options.transition,
layoutTransition: this.options.layoutTransition,
dir: this.options.dir,
components: {
ErrorPage: this.options.ErrorPage
? this.relativeToBuild(this.options.ErrorPage)
: null
}
}
await Promise.all([
this.resolveLayouts(templateContext),
this.resolveRoutes(templateContext),
this.resolveStore(templateContext),
this.resolveMiddleware(templateContext)
])
// -- Layouts --
await this.resolveCustomTemplates(templateContext)
await this.resolveLoadingIndicator(templateContext)
// Add vue-app template dir to watchers
this.options.build.watch.push(this.template.dir)
await this.compileTemplates(templateContext)
consola.success('Nuxt files generated')
}
normalizePlugins() {
return uniqBy(
this.options.plugins.map((p) => {
if (typeof p === 'string') {
p = { src: p }
}
const pluginBaseName = path.basename(p.src, path.extname(p.src)).replace(
/[^a-zA-Z?\d\s:]/g,
''
)
if (p.ssr === false) {
p.mode = 'client'
} else if (p.mode === undefined) {
p.mode = 'all'
} else if (!['client', 'server', 'all'].includes(p.mode)) {
consola.warn(`Invalid plugin mode (server/client/all): '${p.mode}'. Falling back to 'all'`)
p.mode = 'all'
}
return {
src: this.nuxt.resolver.resolveAlias(p.src),
mode: p.mode,
name: 'nuxt_plugin_' + pluginBaseName + '_' + hash(p.src)
}
}),
p => p.name
)
}
async resolveFiles(dir, cwd = this.options.srcDir) {
return this.ignore.filter(await glob(`${dir}/**/*.{${this.supportedExtensions.join(',')}}`, {
cwd,
ignore: this.options.ignore
}))
}
async resolveRelative(dir) {
const dirPrefix = new RegExp(`^${dir}/`)
return (await this.resolveFiles(dir)).map(file => ({ src: file.replace(dirPrefix, '') }))
}
async resolveLayouts({ templateVars, templateFiles }) {
if (fsExtra.existsSync(path.resolve(this.options.srcDir, this.options.dir.layouts))) {
const configLayouts = this.options.layouts
const layoutsFiles = await this.resolveFiles(this.options.dir.layouts)
layoutsFiles.forEach((file) => {
for (const file of await this.resolveFiles(this.options.dir.layouts)) {
const name = file
.replace(new RegExp(`^${this.options.dir.layouts}/`), '')
.replace(new RegExp(`\\.(${this.supportedExtensions.join('|')})$`), '')
// Layout Priority: module.addLayout > .vue file > other extensions
if (name === 'error') {
if (!templateVars.components.ErrorPage) {
templateVars.components.ErrorPage = this.relativeToBuild(
@ -370,27 +316,26 @@ export default class Builder {
file
)
}
return
}
// Layout Priority: module.addLayout > .vue file > other extensions
if (configLayouts[name]) {
consola.warn(`Duplicate layout registration, "${name}" has been registered as "${configLayouts[name]}"`)
} else if (this.options.layouts[name]) {
consola.warn(`Duplicate layout registration, "${name}" has been registered as "${this.options.layouts[name]}"`)
} else if (!templateVars.layouts[name] || /\.vue$/.test(file)) {
templateVars.layouts[name] = this.relativeToBuild(
this.options.srcDir,
file
)
}
})
}
}
// If no default layout, create its folder and add the default folder
if (!templateVars.layouts.default) {
await fsExtra.mkdirp(r(this.options.buildDir, 'layouts'))
templatesFiles.push('layouts/default.vue')
templateFiles.push('layouts/default.vue')
templateVars.layouts.default = './layouts/default.vue'
}
}
// -- Routes --
async resolveRoutes({ templateVars }) {
consola.debug('Generating routes...')
if (this._defaultPage) {
@ -443,8 +388,9 @@ export default class Builder {
// Make routes accessible for other modules and webpack configs
this.routes = templateVars.router.routes
}
// -- Store --
async resolveStore({ templateVars, templateFiles }) {
// Add store if needed
if (this.options.store) {
templateVars.storeModules = (await this.resolveRelative(this.options.dir.store))
@ -459,51 +405,55 @@ export default class Builder {
return res
})
templatesFiles.push('store.js')
templateFiles.push('store.js')
}
}
async resolveMiddleware({ templateVars }) {
// -- Middleware --
templateVars.middleware = await this.resolveRelative(this.options.dir.middleware)
}
async resolveCustomTemplates(templateContext) {
// Resolve template files
const customTemplateFiles = this.options.build.templates.map(
t => t.dst || path.basename(t.src || t)
)
templatesFiles = templatesFiles
.map((file) => {
// Skip if custom file was already provided in build.templates[]
if (customTemplateFiles.includes(file)) {
return
}
// Allow override templates using a file with same name in ${srcDir}/app
const customPath = r(this.options.srcDir, 'app', file)
const customFileExists = fsExtra.existsSync(customPath)
const templateFiles = await Promise.all(templateContext.templateFiles.map(async (file) => {
// Skip if custom file was already provided in build.templates[]
if (customTemplateFiles.includes(file)) {
return
}
// Allow override templates using a file with same name in ${srcDir}/app
const customPath = r(this.options.srcDir, 'app', file)
const customFileExists = await fsExtra.exists(customPath)
return {
src: customFileExists ? customPath : r(this.template.dir, file),
dst: file,
custom: customFileExists
}
})
return {
src: customFileExists ? customPath : r(this.template.dir, file),
dst: file,
custom: customFileExists
}
}))
templateContext.templateFiles = templateFiles
.filter(Boolean)
// Add custom template files
.concat(
this.options.build.templates.map((t) => {
return Object.assign(
{
src: r(this.options.srcDir, t.src || t),
dst: t.dst || path.basename(t.src || t),
custom: true
},
t
)
})
)
}
// -- Custom templates --
// Add custom template files
templatesFiles = templatesFiles.concat(
this.options.build.templates.map((t) => {
return Object.assign(
{
src: r(this.options.srcDir, t.src || t),
dst: t.dst || path.basename(t.src || t),
custom: true
},
t
)
})
)
// -- Loading indicator --
async resolveLoadingIndicator({ templateFiles }) {
if (this.options.loadingIndicator.name) {
let indicatorPath = path.resolve(
this.template.dir,
@ -512,7 +462,7 @@ export default class Builder {
)
let customIndicator = false
if (!fsExtra.existsSync(indicatorPath)) {
if (!await fsExtra.exists(indicatorPath)) {
indicatorPath = this.nuxt.resolver.resolveAlias(
this.options.loadingIndicator.name
)
@ -525,63 +475,42 @@ export default class Builder {
}
if (indicatorPath) {
templatesFiles.push({
templateFiles.push({
src: indicatorPath,
dst: 'loading.html',
custom: customIndicator,
options: this.options.loadingIndicator
})
} else {
/* istanbul ignore next */
// eslint-disable-next-line no-console
console.error(
consola.error(
`Could not fetch loading indicator: ${
this.options.loadingIndicator.name
}`
)
}
}
}
async compileTemplates(templateContext) {
// Prepare template options
const { templateVars, templateFiles, templateOptions } = templateContext
await this.nuxt.callHook('build:templates', {
templatesFiles,
templateVars,
templateFiles,
resolve: r
})
// Prepare template options
let lodash = null
const templateOptions = {
imports: {
serialize,
serializeFunction,
devalue,
hash,
r,
wp,
wChunk,
resolvePath: this.nuxt.resolver.resolvePath,
resolveAlias: this.nuxt.resolver.resolveAlias,
relativeToBuild: this.relativeToBuild,
// Legacy support: https://github.com/nuxt/nuxt.js/issues/4350
_: new Proxy({}, {
get(target, prop) {
if (!lodash) {
consola.warn('Avoid using _ inside templates')
lodash = require('lodash')
}
return lodash[prop]
}
})
},
interpolate: /<%=([\s\S]+?)%>/g
templateOptions.imports = {
...templateOptions.imports,
resolvePath: this.nuxt.resolver.resolvePath,
resolveAlias: this.nuxt.resolver.resolveAlias,
relativeToBuild: this.relativeToBuild
}
// Add vue-app template dir to watchers
this.options.build.watch.push(this.template.dir)
// Interpret and move template files to .nuxt/
await Promise.all(
templatesFiles.map(async ({ src, dst, options, custom }) => {
templateFiles.map(async ({ src, dst, options, custom }) => {
// Add custom templates to watcher
if (custom) {
this.options.build.watch.push(src)
@ -603,7 +532,6 @@ export default class Builder {
)
)
} catch (err) {
/* istanbul ignore next */
throw new Error(`Could not compile template ${src}: ${err.message}`)
}
const _path = r(this.options.buildDir, dst)
@ -611,8 +539,36 @@ export default class Builder {
await fsExtra.outputFile(_path, content, 'utf8')
})
)
}
consola.success('Nuxt files generated')
resolvePlugins() {
// Check plugins exist then set alias to their real path
return Promise.all(this.plugins.map(async (p) => {
const ext = '{?(.+([^.])),/index.+([^.])}'
const pluginFiles = await glob(`${p.src}${ext}`)
if (!pluginFiles || pluginFiles.length === 0) {
throw new Error(`Plugin not found: ${p.src}`)
}
if (pluginFiles.length > 1 && !isIndexFileAndFolder(pluginFiles)) {
consola.warn({
message: `Found ${pluginFiles.length} plugins that match the configuration, suggest to specify extension:`,
additional: '\n' + pluginFiles.map(x => `- ${x}`).join('\n')
})
}
const modes = ['client', 'server']
const modePattern = new RegExp(`\\.(${modes.join('|')})\\.\\w+$`)
pluginFiles[0].replace(modePattern, (_, mode) => {
// mode in nuxt.config has higher priority
if (p.mode === 'all' && modes.includes(mode)) {
p.mode = mode
}
})
p.src = this.relativeToBuild(p.src)
}))
}
// TODO: Uncomment when generateConfig enabled again

View File

@ -0,0 +1,76 @@
import hash from 'hash-sum'
import consola from 'consola'
import uniqBy from 'lodash/uniqBy'
import serialize from 'serialize-javascript'
import devalue from '@nuxt/devalue'
import { r, wp, wChunk, serializeFunction } from '@nuxt/utils'
export default class TemplateContext {
constructor(builder, options) {
this.templateFiles = Array.from(builder.template.files)
this.templateVars = {
options: options,
extensions: options.extensions
.map(ext => ext.replace(/^\./, ''))
.join('|'),
messages: options.messages,
splitChunks: options.build.splitChunks,
uniqBy,
isDev: options.dev,
isTest: options.test,
debug: options.debug,
vue: { config: options.vue.config },
mode: options.mode,
router: options.router,
env: options.env,
head: options.head,
store: options.store,
globalName: options.globalName,
globals: builder.globals,
css: options.css,
plugins: builder.plugins,
appPath: './App.js',
layouts: Object.assign({}, options.layouts),
loading:
typeof options.loading === 'string'
? builder.relativeToBuild(options.srcDir, options.loading)
: options.loading,
transition: options.transition,
layoutTransition: options.layoutTransition,
dir: options.dir,
components: {
ErrorPage: options.ErrorPage
? builder.relativeToBuild(options.ErrorPage)
: null
}
}
}
get templateOptions() {
let lodash = null
return {
imports: {
serialize,
serializeFunction,
devalue,
hash,
r,
wp,
wChunk,
// Legacy support: https://github.com/nuxt/nuxt.js/issues/4350
_: new Proxy({}, {
get(target, prop) {
if (!lodash) {
consola.warn('Avoid using _ inside templates')
lodash = require('lodash')
}
return lodash[prop]
}
})
},
interpolate: /<%=([\s\S]+?)%>/g
}
}
}