2020-07-02 13:02:35 +00:00
import path from 'path'
import fs from 'fs'
import hash from 'hash-sum'
import consola from 'consola'
2020-08-02 15:50:35 +00:00
import type { NormalizedConfiguration } from 'nuxt/config'
import { chainFn , Mode , sequence } from 'nuxt/utils'
2020-07-02 13:02:35 +00:00
2020-07-30 23:40:16 +00:00
import Nuxt from './nuxt'
2020-08-02 15:50:35 +00:00
import type { NuxtModule , ModuleHandler } from 'nuxt/config/config/_common'
2020-07-30 23:40:16 +00:00
2020-08-02 15:50:35 +00:00
interface TemplateInput {
filename? : string
fileName? : string
options? : Record < string , any >
2020-07-30 23:40:16 +00:00
src : string
2020-08-02 15:50:35 +00:00
ssr? : boolean
mode ? : 'all' | 'server' | 'client'
2020-07-30 23:40:16 +00:00
}
2020-07-02 13:02:35 +00:00
export default class ModuleContainer {
2020-07-30 23:40:16 +00:00
nuxt : Nuxt
options : Nuxt [ 'options' ]
2020-08-02 15:50:35 +00:00
requiredModules : Record < string , {
src : string
options : Record < string , any >
handler : ModuleHandler
} >
2020-07-30 23:40:16 +00:00
2020-08-02 15:50:35 +00:00
constructor ( nuxt : Nuxt ) {
2020-07-02 13:02:35 +00:00
this . nuxt = nuxt
this . options = nuxt . options
this . requiredModules = { }
// Self bind to allow destructre from container
for ( const method of Object . getOwnPropertyNames ( ModuleContainer . prototype ) ) {
if ( typeof this [ method ] === 'function' ) {
this [ method ] = this [ method ] . bind ( this )
}
}
}
2020-08-02 15:50:35 +00:00
async ready() {
2020-07-02 13:02:35 +00:00
// Call before hook
await this . nuxt . callHook ( 'modules:before' , this , this . options . module s )
if ( this . options . buildModules && ! this . options . _start ) {
// Load every devModule in sequence
await sequence ( this . options . buildModules , this . addModule )
}
// Load every module in sequence
await sequence ( this . options . module s , this . addModule )
// Load ah-hoc modules last
await sequence ( this . options . _modules , this . addModule )
// Call done hook
await this . nuxt . callHook ( 'modules:done' , this )
}
2020-08-02 15:50:35 +00:00
addVendor() {
2020-07-02 13:02:35 +00:00
consola . warn ( 'addVendor has been deprecated due to webpack4 optimization' )
}
2020-08-02 15:50:35 +00:00
addTemplate ( template : TemplateInput | string ) {
2020-07-02 13:02:35 +00:00
if ( ! template ) {
throw new Error ( 'Invalid template: ' + JSON . stringify ( template ) )
}
// Validate & parse source
2020-08-02 15:50:35 +00:00
const src = typeof template === 'string' ? template : template.src
2020-07-02 13:02:35 +00:00
const srcPath = path . parse ( src )
if ( typeof src !== 'string' || ! fs . existsSync ( src ) ) {
throw new Error ( 'Template src not found: ' + src )
}
// Mostly for DX, some people prefers `filename` vs `fileName`
2020-08-02 15:50:35 +00:00
const fileName = typeof template === 'string' ? '' : template . fileName || template . filename
2020-07-02 13:02:35 +00:00
// Generate unique and human readable dst filename if not provided
const dst = fileName || ` ${ path . basename ( srcPath . dir ) } . ${ srcPath . name } . ${ hash ( src ) } ${ srcPath . ext } `
// Add to templates list
const templateObj = {
src ,
dst ,
2020-08-02 15:50:35 +00:00
options : typeof template === 'string' ? undefined : template . options
2020-07-02 13:02:35 +00:00
}
this . options . build . templates . push ( templateObj )
return templateObj
}
2020-08-02 15:50:35 +00:00
addPlugin ( template : TemplateInput ) {
2020-07-02 13:02:35 +00:00
const { dst } = this . addTemplate ( template )
// Add to nuxt plugins
this . options . plugins . unshift ( {
src : path.join ( this . options . buildDir , dst ) ,
// TODO: remove deprecated option in Nuxt 3
ssr : template.ssr ,
mode : template.mode
} )
}
2020-08-02 15:50:35 +00:00
addLayout ( template : TemplateInput , name : string ) {
2020-07-02 13:02:35 +00:00
const { dst , src } = this . addTemplate ( template )
const layoutName = name || path . parse ( src ) . name
const layout = this . options . layouts [ layoutName ]
if ( layout ) {
consola . warn ( ` Duplicate layout registration, " ${ layoutName } " has been registered as " ${ layout } " ` )
}
// Add to nuxt layouts
this . options . layouts [ layoutName ] = ` ./ ${ dst } `
// If error layout, set ErrorPage
if ( name === 'error' ) {
this . addErrorLayout ( dst )
}
}
2020-08-02 15:50:35 +00:00
addErrorLayout ( dst : string ) {
2020-07-02 13:02:35 +00:00
const relativeBuildDir = path . relative ( this . options . rootDir , this . options . buildDir )
this . options . ErrorPage = ` ~/ ${ relativeBuildDir } / ${ dst } `
}
2020-08-02 15:50:35 +00:00
addServerMiddleware ( middleware : NormalizedConfiguration [ 'serverMiddleware' ] [ number ] ) {
2020-07-02 13:02:35 +00:00
this . options . serverMiddleware . push ( middleware )
}
2020-08-02 15:50:35 +00:00
extendBuild ( fn : NormalizedConfiguration [ 'build' ] [ 'extend' ] ) {
2020-07-02 13:02:35 +00:00
this . options . build . extend = chainFn ( this . options . build . extend , fn )
}
2020-08-02 15:50:35 +00:00
extendRoutes ( fn : NormalizedConfiguration [ 'router' ] [ 'extendRoutes' ] ) {
2020-07-02 13:02:35 +00:00
this . options . router . extendRoutes = chainFn (
this . options . router . extendRoutes ,
fn
)
}
2020-08-02 15:50:35 +00:00
requireModule ( module Opts : NuxtModule ) {
2020-07-02 13:02:35 +00:00
return this . addModule ( module Opts )
}
2020-08-02 15:50:35 +00:00
async addModule ( module Opts : NuxtModule ) {
let src : string | ModuleHandler
2020-07-30 23:40:16 +00:00
let options : Record < string , any >
2020-08-02 15:50:35 +00:00
let handler : ModuleHandler | ModuleHandler & { meta : { name : string } }
2020-07-02 13:02:35 +00:00
// Type 1: String or Function
if ( typeof module Opts === 'string' || typeof module Opts === 'function' ) {
src = module Opts
} else if ( Array . isArray ( module Opts ) ) {
// Type 2: Babel style array
[ src , options ] = module Opts
} else if ( typeof module Opts === 'object' ) {
// Type 3: Pure object
( { src , options , handler } = module Opts )
}
// Define handler if src is a function
2020-07-30 23:40:16 +00:00
if ( src instanceof Function ) {
2020-07-02 13:02:35 +00:00
handler = src
}
// Prevent adding buildModules-listed entries in production
if ( this . options . buildModules . includes ( handler ) && this . options . _start ) {
return
}
// Resolve handler
2020-08-02 15:50:35 +00:00
if ( ! handler && typeof src === 'string' ) {
2020-07-02 13:02:35 +00:00
try {
handler = this . nuxt . resolver . requireModule ( src , { useESM : true } )
} catch ( error ) {
if ( error . code !== 'MODULE_NOT_FOUND' ) {
throw error
}
// Hint only if entrypoint is not found and src is not local alias or path
if ( error . message . includes ( src ) && ! /^[~.]|^@\// . test ( src ) ) {
let message = 'Module `{name}` not found.'
if ( this . options . buildModules . includes ( src ) ) {
message += ' Please ensure `{name}` is in `devDependencies` and installed. HINT: During build step, for npm/yarn, `NODE_ENV=production` or `--production` should NOT be used.' . replace ( '{name}' , src )
} else if ( this . options . module s.includes ( src ) ) {
message += ' Please ensure `{name}` is in `dependencies` and installed.'
}
message = message . replace ( /{name}/g , src )
consola . warn ( message )
}
if ( this . options . _cli ) {
throw error
} else {
// TODO: Remove in next major version
consola . warn ( 'Silently ignoring module as programatic usage detected.' )
return
}
}
}
// Validate handler
if ( typeof handler !== 'function' ) {
throw new TypeError ( 'Module should export a function: ' + src )
}
// Ensure module is required once
2020-08-02 15:50:35 +00:00
if ( 'meta' in handler && typeof src === 'string' ) {
const metaKey = handler . meta && handler . meta . name
const key = metaKey || src
if ( typeof key === 'string' ) {
if ( this . requiredModules [ key ] ) {
if ( ! metaKey ) {
// TODO: Skip with nuxt3
consola . warn ( 'Modules should be only specified once:' , key )
} else {
return
}
2020-07-02 13:02:35 +00:00
}
2020-08-02 15:50:35 +00:00
this . requiredModules [ key ] = { src , options , handler : handler as ModuleHandler }
2020-07-02 13:02:35 +00:00
}
}
// Default module options to empty object
if ( options === undefined ) {
options = { }
}
const result = await handler . call ( this , options )
return result
}
}