feat: improve typing of config

This commit is contained in:
Daniel Roe 2020-08-02 16:50:35 +01:00
parent 0d5d854c63
commit 212283837b
29 changed files with 1044 additions and 421 deletions

View File

@ -254,7 +254,7 @@ export default class Builder {
} }
} }
globPathWithExtensions (path) { globPathWithExtensions(path: string) {
return `${path}/**/*.{${this.supportedExtensions.join(',')}}` return `${path}/**/*.{${this.supportedExtensions.join(',')}}`
} }
@ -278,7 +278,7 @@ export default class Builder {
this.resolveMiddleware(templateContext) this.resolveMiddleware(templateContext)
]) ])
await this.resolvePlugins(templateContext) await this.resolvePlugins()
this.addOptionalTemplates(templateContext) this.addOptionalTemplates(templateContext)
@ -293,7 +293,7 @@ export default class Builder {
async normalizePlugins () { async normalizePlugins () {
// options.extendPlugins allows for returning a new plugins array // options.extendPlugins allows for returning a new plugins array
if (typeof this.options.extendPlugins === 'function') { if (this.options.extendPlugins instanceof Function) {
const extendedPlugins = this.options.extendPlugins(this.options.plugins) const extendedPlugins = this.options.extendPlugins(this.options.plugins)
if (Array.isArray(extendedPlugins)) { if (Array.isArray(extendedPlugins)) {
@ -340,7 +340,7 @@ export default class Builder {
) )
} }
addOptionalTemplates (templateContext) { addOptionalTemplates(templateContext) {
if (this.options.build.indicator) { if (this.options.build.indicator) {
// templateContext.templateFiles.push('components/nuxt-build-indicator.vue') // templateContext.templateFiles.push('components/nuxt-build-indicator.vue')
} }
@ -350,19 +350,19 @@ export default class Builder {
} }
} }
async resolveFiles (dir, cwd = this.options.srcDir) { async resolveFiles(dir, cwd = this.options.srcDir) {
return this.ignore.filter(await glob(this.globPathWithExtensions(dir), { return this.ignore.filter(await glob(this.globPathWithExtensions(dir), {
cwd, cwd,
follow: this.options.build.followSymlinks follow: this.options.build.followSymlinks
})) }))
} }
async resolveRelative (dir) { async resolveRelative(dir) {
const dirPrefix = new RegExp(`^${dir}/`) const dirPrefix = new RegExp(`^${dir}/`)
return (await this.resolveFiles(dir)).map(file => ({ src: file.replace(dirPrefix, '') })) return (await this.resolveFiles(dir)).map(file => ({ src: file.replace(dirPrefix, '') }))
} }
async resolveApp ({ templateVars }) { async resolveApp({ templateVars }) {
templateVars.appPath = 'nuxt-app/app.tutorial.vue' templateVars.appPath = 'nuxt-app/app.tutorial.vue'
for (const appFile of this.appFiles) { for (const appFile of this.appFiles) {
@ -376,7 +376,7 @@ export default class Builder {
templateVars.hasApp = false templateVars.hasApp = false
} }
async resolveLayouts ({ templateVars, templateFiles }) { async resolveLayouts({ templateVars, templateFiles }) {
if (!this.options.features.layouts) { if (!this.options.features.layouts) {
return return
} }
@ -414,7 +414,7 @@ export default class Builder {
} }
} }
async resolvePages (templateContext) { async resolvePages(templateContext) {
const { templateVars } = templateContext const { templateVars } = templateContext
const pagesDir = path.join(this.options.srcDir, this.options.dir.pages) const pagesDir = path.join(this.options.srcDir, this.options.dir.pages)
@ -475,7 +475,7 @@ export default class Builder {
this.routes = templateVars.router.routes this.routes = templateVars.router.routes
} }
async resolveStore ({ templateVars, templateFiles }) { async resolveStore({ templateVars, templateFiles }) {
// Add store if needed // Add store if needed
if (!this.options.features.store || !this.options.store) { if (!this.options.features.store || !this.options.store) {
return return
@ -515,7 +515,7 @@ export default class Builder {
async resolveCustomTemplates (templateContext) { async resolveCustomTemplates (templateContext) {
// Sanitize custom template files // Sanitize custom template files
this.options.build.templates = this.options.build.templates.map((t) => { this.options.build.templates = this.options.build.templates.map((t) => {
const src = t.src || t const src = typeof t === 'string' ? t : t.src
return { return {
src: r(this.options.srcDir, src), src: r(this.options.srcDir, src),
dst: t.dst || path.basename(src), dst: t.dst || path.basename(src),
@ -524,7 +524,7 @@ export default class Builder {
} }
}) })
const customTemplateFiles = this.options.build.templates.map(t => t.dst || path.basename(t.src || t)) const customTemplateFiles = this.options.build.templates.map(t => t.dst || path.basename(typeof t === 'string' ? t : t.src))
const templatePaths = uniq([ const templatePaths = uniq([
// Modules & user provided templates // Modules & user provided templates
@ -559,7 +559,7 @@ export default class Builder {
} }
async resolveLoadingIndicator ({ templateFiles }) { async resolveLoadingIndicator ({ templateFiles }) {
if (!this.options.loadingIndicator.name) { if (typeof this.options.loadingIndicator !== 'object' || !this.options.loadingIndicator.name) {
return return
} }
let indicatorPath = path.resolve( let indicatorPath = path.resolve(
@ -671,7 +671,7 @@ export default class Builder {
} }
// TODO: Uncomment when generateConfig enabled again // TODO: Uncomment when generateConfig enabled again
// async generateConfig() { // async generateConfig () {
// const config = path.resolve(this.options.buildDir, 'build.config.js') // const config = path.resolve(this.options.buildDir, 'build.config.js')
// const options = omit(this.options, Options.unsafeKeys) // const options = omit(this.options, Options.unsafeKeys)
// await fsExtra.writeFile( // await fsExtra.writeFile(
@ -707,7 +707,7 @@ export default class Builder {
} }
} }
assignWatcher (key) { assignWatcher (key: string) {
return (watcher) => { return (watcher) => {
if (this.watchers[key]) { if (this.watchers[key]) {
this.watchers[key].close() this.watchers[key].close()
@ -731,7 +731,7 @@ export default class Builder {
patterns.push(r(this.options.srcDir, this.options.dir.pages)) patterns.push(r(this.options.srcDir, this.options.dir.pages))
} }
patterns = patterns.map((path, ...args) => upath.normalizeSafe(this.globPathWithExtensions(path), ...args)) patterns = patterns.map((path) => upath.normalizeSafe(this.globPathWithExtensions(path)))
const refreshFiles = debounce(() => this.generateRoutesAndFiles(), 200) const refreshFiles = debounce(() => this.generateRoutesAndFiles(), 200)

View File

@ -3,6 +3,7 @@ import uniqBy from 'lodash/uniqBy'
import serialize from 'serialize-javascript' import serialize from 'serialize-javascript'
import devalue from '@nuxt/devalue' import devalue from '@nuxt/devalue'
import { NormalizedConfiguration } from 'nuxt/config'
import { r, wp, wChunk, serializeFunction, isFullStatic } from 'nuxt/utils' import { r, wp, wChunk, serializeFunction, isFullStatic } from 'nuxt/utils'
import type Builder from '../builder' import type Builder from '../builder'
@ -11,7 +12,7 @@ export default class TemplateContext {
templateFiles: string[] templateFiles: string[]
templateVars: any templateVars: any
constructor (builder: Builder, options) { constructor(builder: Builder, options: NormalizedConfiguration) {
this.templateFiles = Array.from(builder.template.files) this.templateFiles = Array.from(builder.template.files)
this.templateVars = { this.templateVars = {
nuxtOptions: options, nuxtOptions: options,
@ -57,7 +58,7 @@ export default class TemplateContext {
} }
} }
get templateOptions () { get templateOptions() {
return { return {
imports: { imports: {
serialize, serialize,

View File

@ -1,21 +1,29 @@
import path from 'path' import path from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
import ignore from 'ignore' import ignore from 'ignore'
import { NormalizedConfiguration } from 'nuxt/config'
type IgnoreInstance = ReturnType<typeof ignore> type IgnoreInstance = ReturnType<typeof ignore>
type IgnoreOptions = Parameters<typeof ignore>[0] type IgnoreOptions = Parameters<typeof ignore>[0]
interface Options {
rootDir: string
ignore?: IgnoreInstance
ignoreArray?: Array<string | IgnoreInstance>
ignoreOptions?: IgnoreOptions
}
export default class Ignore { export default class Ignore {
rootDir: string rootDir: string
ignore?: IgnoreInstance ignore?: IgnoreInstance
ignoreArray?: string | string ignoreArray?: Array<string | IgnoreInstance>
ignoreFile?: string ignoreFile?: string
ignoreOptions?: IgnoreOptions ignoreOptions?: IgnoreOptions
constructor (options) { constructor ({ ignoreArray, ignoreOptions, rootDir }: Options) {
this.rootDir = options.rootDir this.rootDir = rootDir
this.ignoreOptions = options.ignoreOptions this.ignoreOptions = ignoreOptions
this.ignoreArray = options.ignoreArray this.ignoreArray = ignoreArray
this.addIgnoresRules() this.addIgnoresRules()
} }

View File

@ -1,13 +1,13 @@
import path from 'path' import path from 'path'
import consola from 'consola' import consola from 'consola'
import minimist, { ParsedArgs } from 'minimist' import minimist, { Opts as MinimistOptions, ParsedArgs } from 'minimist'
import Hookable from 'hookable' import Hookable from 'hookable'
import { Nuxt } from 'nuxt/core'
import { Builder } from 'nuxt/builder' import { Builder } from 'nuxt/builder'
import { CliConfiguration } from 'nuxt/config/options'
import { Nuxt } from 'nuxt/core'
import { Generator } from 'nuxt/generator' import { Generator } from 'nuxt/generator'
import type { Target } from 'nuxt/utils'
import { name, version } from '../../package.json' import { name, version } from '../../package.json'
@ -26,16 +26,6 @@ export interface Command {
type Hooks = Parameters<Hookable['addHooks']>[0] type Hooks = Parameters<Hookable['addHooks']>[0]
interface ExtraOptions {
_build?: boolean
_cli?: boolean
_export?: boolean
_generate?: boolean
_start?: boolean
dev?: boolean
server?: boolean
target?: Target
}
export default class NuxtCommand extends Hookable { export default class NuxtCommand extends Hookable {
_argv: string[] _argv: string[]
@ -133,7 +123,7 @@ export default class NuxtCommand extends Hookable {
return this._parsedArgv return this._parsedArgv
} }
async getNuxtConfig (extraOptions: ExtraOptions = {}) { async getNuxtConfig(extraOptions: Partial<CliConfiguration> = {}) {
// Flag to indicate nuxt is running with CLI (not programmatic) // Flag to indicate nuxt is running with CLI (not programmatic)
extraOptions._cli = true extraOptions._cli = true
@ -154,7 +144,7 @@ export default class NuxtCommand extends Hookable {
return options return options
} }
async getNuxt (options) { async getNuxt (options: CliConfiguration) {
const nuxt = new Nuxt(options) const nuxt = new Nuxt(options)
await nuxt.ready() await nuxt.ready()
@ -166,12 +156,12 @@ export default class NuxtCommand extends Hookable {
return new Builder(nuxt) return new Builder(nuxt)
} }
async getGenerator (nuxt) { async getGenerator (nuxt: Nuxt) {
const builder = await this.getBuilder(nuxt) const builder = await this.getBuilder(nuxt)
return new Generator(nuxt, builder) return new Generator(nuxt, builder)
} }
async setLock (lockRelease) { async setLock (lockRelease?: () => Promise<any>) {
if (lockRelease) { if (lockRelease) {
if (this._lockRelease) { if (this._lockRelease) {
consola.warn(`A previous unreleased lock was found, this shouldn't happen and is probably an error in 'nuxt ${this.cmd.name}' command. The lock will be removed but be aware of potential strange results`) consola.warn(`A previous unreleased lock was found, this shouldn't happen and is probably an error in 'nuxt ${this.cmd.name}' command. The lock will be removed but be aware of potential strange results`)
@ -200,7 +190,7 @@ export default class NuxtCommand extends Hookable {
} }
_getMinimistOptions () { _getMinimistOptions () {
const minimistOptions = { const minimistOptions: MinimistOptions = {
alias: {}, alias: {},
boolean: [], boolean: [],
string: [], string: [],
@ -225,7 +215,7 @@ export default class NuxtCommand extends Hookable {
} }
_getHelp () { _getHelp () {
const options = [] const options: [string, string][] = []
let maxOptionLength = 0 let maxOptionLength = 0
for (const name in this.cmd.options) { for (const name in this.cmd.options) {

View File

@ -1,11 +1,16 @@
import consola from 'consola' import consola from 'consola'
import type { ParsedArgs} from 'minimist'
import { Configuration } from 'nuxt/config/options'
import NuxtCommand from '../command'
export default { export default {
port: { port: {
alias: 'p', alias: 'p',
type: 'string', type: 'string',
description: 'Port number on which to start the application', description: 'Port number on which to start the application',
prepare (cmd, options, argv) { prepare (_cmd: NuxtCommand, options: Configuration, argv: ParsedArgs) {
if (argv.port) { if (argv.port) {
options.server.port = +argv.port options.server.port = +argv.port
} }
@ -15,7 +20,7 @@ export default {
alias: 'H', alias: 'H',
type: 'string', type: 'string',
description: 'Hostname on which to start the application', description: 'Hostname on which to start the application',
prepare (cmd, options, argv) { prepare (_cmd: NuxtCommand, _options: any, argv: ParsedArgs) {
if (argv.hostname === '') { if (argv.hostname === '') {
consola.fatal('Provided hostname argument has no value') consola.fatal('Provided hostname argument has no value')
} }

View File

@ -1,7 +1,72 @@
export default () => ({ import type { App } from 'vue'
import type { MetaInfo, VueMetaOptions } from 'vue-meta'
type Plugin = string | { mode?: 'all' | 'client' | 'server', src: string, ssr?: boolean }
interface AppOptions {
css: string[]
head: MetaInfo
ErrorPage: null | string
extendPlugins: null | ((plugins: Plugin[]) => Plugin[])
features: {
store: boolean
layouts: boolean
meta: boolean
middleware: boolean
transitions: boolean
deprecations: boolean
validate: boolean
asyncData: boolean
fetch: boolean
clientOnline: boolean
clientPrefetch: boolean
clientUseUrl: boolean
componentAliases: boolean
componentClientOnly: boolean
}
fetch: {
server: boolean
client: boolean
}
layouts: {}
layoutTransition: {
name: string
mode?: string | 'out-in'
}
loading: string | false | {
color?: string
continuous?: boolean
css?: boolean
duration?: number
failedColor?: string
height?: string
rtl?: boolean
throttle?: number
}
loadingIndicator: string | false | {
background?: string
color?: string
color2?: string
name?: string
}
pageTransition: {
name: string
mode?: string | 'out-in'
appear?: boolean
appearClass?: string
appearActiveClass?: string
appearToClass?: string
}
plugins: Array<Plugin>
vue: {
config: Partial<App['config']>
}
vueMeta: null | VueMetaOptions
}
export default (): AppOptions => ({
vue: { vue: {
config: { config: {
silent: undefined, // = !dev
performance: undefined // = dev performance: undefined // = dev
} }
}, },
@ -74,3 +139,34 @@ export default () => ({
componentClientOnly: true componentClientOnly: true
} }
}) })
// type NormalizedConfiguration<T extends Record<string, any>> = T & {
// pageTransition?: Exclude<T['pageTransition'], string>
// layoutTransition?: Exclude<T['layoutTransition'], string>
// extensions?: Exclude<T['extensions'], string>
// }
// export function normalizeAppConfig<O extends Configuration>(options: O): asserts options is NormalizedConfiguration<O> {
// (options as NormalizedConfiguration<O>).__normalized__ = true
// // Normalize options
// if (options.loading === true) {
// delete options.loading
// }
// if (options.router && typeof options.router.base === 'string') {
// (options as NormalizedConfiguration<O>)._routerBaseSpecified = true
// }
// if (typeof options.pageTransition === 'string') {
// options.pageTransition = { name: options.pageTransition }
// }
// if (typeof options.layoutTransition === 'string') {
// options.layoutTransition = { name: options.layoutTransition }
// }
// if (typeof options.extensions === 'string') {
// options.extensions = [options.extensions]
// }
// }

View File

@ -1,8 +1,133 @@
import type { WatchOptions as ChokidarWatchOptions } from 'chokidar'
import type { NextHandleFunction, Server as ConnectServer } from 'connect'
import type { configHooksT } from 'hookable/types/types'
import ignore from 'ignore'
import capitalize from 'lodash/capitalize' import capitalize from 'lodash/capitalize'
import env from 'std-env' import env from 'std-env'
import { TARGETS, MODES } from 'nuxt/utils' import type { Configuration as WebpackConfiguration } from 'webpack'
export default () => ({ import { TARGETS, MODES, Target, Mode } from 'nuxt/utils'
import type { NormalizedConfiguration } from '../options'
import Hookable from 'hookable'
type IgnoreOptions = Parameters<typeof ignore>[0]
type IgnoreInstance = ReturnType<typeof ignore>
interface ExtendFunctionContext {
isClient: boolean
isDev: boolean
isLegacy: boolean
isModern: boolean
isServer: boolean
// TODO
// loaders: NuxtOptionsLoaders
}
type ExtendFunction = (config: WebpackConfiguration, ctx: ExtendFunctionContext) => void
interface NuxtHooks extends configHooksT {
build?: {
before?(builder: any, buildOptions: any): void
compile?(params: { name: 'client' | 'server', compiler: any }): void
compiled?(params: { name: 'client' | 'server', compiler: any, stats: any }): void
done?(builder: any): void
extendRoutes?(routes: any, resolve: any): void
templates?(params: { templateFiles: any, templateVars: any, resolve: any }): void
}
close?(nuxt: any): void
error?(error: Error): void
generate?: {
before?(generator: any, generateOptions: any): void
distCopied?(generator: any): void
distRemoved?(generator: any): void
done?(generator: any): void
extendRoutes?(routes: any): void
page?(params: { route: any, path: any, html: any }): void
routeCreated?(route: any, path: any, errors: any): void
routeFailed?(route: any, errors: any): void
}
listen?(server: any, params: { host: string, port: number | string }): void
modules?: {
before?(moduleContainer: any, options: any): void
done?(moduleContainer: any): void
}
ready?(nuxt: any): void
render?: {
before?(renderer: any, options: any): void
done?(renderer: any): void
errorMiddleware?(app: ConnectServer): void
resourcesLoaded?(resources: any): void
route?(url: string, result: any, context: any): void
routeContext?(context: any): void
routeDone?(url: string, result: any, context: any): void
beforeResponse?(url: string, result: any, context: any): void
setupMiddleware?(app: ConnectServer): void
}
}
interface ModuleThis {
extendBuild(fn: ExtendFunction): void
options: NormalizedConfiguration
nuxt: any // TBD
[key: string]: any // TBD
}
export type ModuleHandler<T = any> = (this: ModuleThis, moduleOptions: T) => Promise<void> | void
export type NuxtModule = string | ModuleHandler | [string | ModuleHandler, any]
export type ServerMiddleware = string | { path: string, prefix?: boolean, handler: string | NextHandleFunction } | NextHandleFunction
interface CommonConfiguration {
_modules: NuxtModule[]
_nuxtConfigFile?: string
alias: Record<string, string>
buildDir: string
buildModules: NuxtModule[]
createRequire?: (module: NodeJS.Module) => NodeJS.Require
debug?: boolean
dev: boolean
dir: { [key in 'app' | 'assets' | 'layouts' | 'middleware' | 'pages' | 'static' | 'store']: string }
editor: undefined
env: NodeJS.ProcessEnv
extensions: string[]
globalName?: string,
globals: {
id: (globalName: string) => string
nuxt: (globalName: string) => string
context: (globalName: string) => string
pluginPrefix: (globalName: string) => string
readyCallback: (globalName: string) => string
loadedCallback: (globalName: string) => string
}
hooks: null | ((hook: Hookable['hook']) => void) | NuxtHooks
ignoreOptions?: IgnoreOptions
ignorePrefix: string
ignore: Array<string | IgnoreInstance>
// TODO: remove in Nuxt 3
mode: Mode
modern?: boolean
modules: NuxtModule[]
privateRuntimeConfig: Record<string, any> | ((env: NodeJS.ProcessEnv) => Record<string, any>)
publicRuntimeConfig: Record<string, any> | ((env: NodeJS.ProcessEnv) => Record<string, any>)
serverMiddleware: Array<ServerMiddleware> | Record<string, NextHandleFunction>
ssr: boolean
target: Target
test: boolean
srcDir?: string
modulesDir: string[]
styleExtensions: string[]
watch: string[]
watchers: {
rewatchOnRawEvents?: boolean
webpack: WebpackConfiguration['watchOptions']
chokidar: ChokidarWatchOptions
}
}
export default (): CommonConfiguration => ({
// Env // Env
dev: Boolean(env.dev), dev: Boolean(env.dev),
test: Boolean(env.test), test: Boolean(env.test),

View File

@ -1,27 +1,261 @@
import env from 'std-env' import env from 'std-env'
import type { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import type { TransformOptions, PluginItem } from '@babel/core'
import type { Options as AutoprefixerOptions } from 'autoprefixer'
import type { Options as FileLoaderOptions } from 'file-loader'
import type { Options as HtmlMinifierOptions } from 'html-minifier'
import type * as Less from 'less'
import type { Options as SassOptions } from 'node-sass'
import type { Options as OptimizeCssAssetsWebpackPluginOptions } from 'optimize-css-assets-webpack-plugin'
import type { Plugin as PostcssPlugin } from 'postcss'
import type { Options as PugOptions } from 'pug'
import type { TerserPluginOptions } from 'terser-webpack-plugin'
import type { VueLoaderOptions } from 'vue-loader'
import type {
Configuration as WebpackConfiguration, WebpackPluginFunction,
} from 'webpack'
import type { Options as WebpackDevMiddlewareOptions } from 'webpack-dev-middleware'
import type { MiddlewareOptions as WebpackHotMiddlewareOptions, ClientOptions as WebpackHotMiddlewareClientOptions } from 'webpack-hot-middleware'
interface WebpackEnv {
isClient: boolean
isDev: boolean
isLegacy: boolean
isModern: boolean
isServer: boolean
}
interface BabelPresetEnv {
envName: 'client' | 'modern' | 'server'
}
interface Warning {
message: string
name: string
}
interface BabelOptions extends Pick<TransformOptions, Exclude<keyof TransformOptions, 'presets' | 'plugins'>> {
cacheCompression?: boolean
cacheDirectory?: boolean
cacheIdentifier?: string
customize?: string | null
presets?: ((env: BabelPresetEnv & WebpackEnv, defaultPreset: [string, object]) => PluginItem[] | void) | PluginItem[] | null
plugins?: ((env: BabelPresetEnv & WebpackEnv) => NonNullable<TransformOptions['plugins']>) | TransformOptions['plugins']
}
type CssLoaderUrlFunction = (url: string, resourcePath: string) => boolean
type CssLoaderImportFunction = (parsedImport: string, resourcePath: string) => boolean
type CssLoaderMode = 'global' | 'local'
interface CssLoaderModulesOptions {
context?: string
getLocalIdent?: (context: string, localIdentName: string, localName: string, options: CssLoaderModulesOptions) => string
hashPrefix?: string
localIdentName?: string
localIdentRegExp?: string | RegExp
mode?: CssLoaderMode
}
interface CssLoaderOptions {
import?: boolean | CssLoaderImportFunction
importLoaders?: number
localsConvention?: 'asIs' | 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly'
modules?: boolean | CssLoaderMode | CssLoaderModulesOptions
onlyLocals?: boolean
sourceMap?: boolean
url?: boolean | CssLoaderUrlFunction
}
interface UrlLoaderOptions {
esModule?: boolean
// TODO
fallback?: any // WebpackLoader
limit?: boolean | number | string
mimetype?: string
}
interface WebpackEnv {
isClient: boolean
isDev: boolean
isLegacy: boolean
isModern: boolean
isServer: boolean
}
interface PostcssOrderPresetFunctions {
cssnanoLast: (names: string[]) => string[]
presetEnvAndCssnanoLast: (names: string[]) => string[]
presetEnvLast: (names: string[]) => string[]
}
type PostcssOrderPreset = keyof PostcssOrderPresetFunctions
interface PostcssVariableMap {
customMedia: Record<string, string>
customProperties: Record<string, string>
customSelectors: Record<string, string>
environmentVariables?: Record<string, string>
}
interface PostcssConfiguration {
order?: PostcssOrderPreset | string[] | ((names: string[], presets: PostcssOrderPresetFunctions) => string[])
plugins?: {
[key: string]: false | { [key: string]: any }
} | ((loader: any) => PostcssPlugin<any>[]) | Array<[string | PostcssPlugin<any>, any] | string | PostcssPlugin<any>>
preset?: {
autoprefixer?: false | AutoprefixerOptions
browsers?: string
exportTo?: string | string[] | Partial<PostcssVariableMap> | ((map: PostcssVariableMap) => Partial<PostcssVariableMap>)
features?: {
[key: string]: boolean | { [key: string]: any }
}
importFrom?: string | string[] | Partial<PostcssVariableMap> | (() => Partial<PostcssVariableMap>)
insertAfter?: { [key: string]: PostcssPlugin<any> }
insertBefore?: { [key: string]: PostcssPlugin<any> }
preserve?: boolean
stage?: 0 | 1 | 2 | 3 | 4 | false
}
}
interface VueStyleOptions {
manualInject?: boolean
ssrId?: boolean
shadowMode?: boolean
}
interface Loaders {
css?: CssLoaderOptions
cssModules?: CssLoaderOptions
file?: FileLoaderOptions
fontUrl?: UrlLoaderOptions
imgUrl?: UrlLoaderOptions
less?: Less.Options
pugPlain?: PugOptions
sass?: SassOptions
scss?: SassOptions
stylus?: any // TBD
vue?: VueLoaderOptions
vueStyle?: {
manualInject?: boolean
ssrId?: boolean
shadowMode?: boolean
}
}
export interface Template {
/**
* Source file. Can be absolute or relative.
*/
src: string,
/**
* Destination file within `.nuxt` filter. This filename should be relative to the project `.nuxt` dir
*/
dst: string,
/**
* Options are provided to template as `options` key
*/
options?: Record<string, any>
}
export default () => ({ export default () => ({
quiet: Boolean(env.ci || env.test), /**
analyze: false, * @private
profile: process.argv.includes('--profile'), */
extractCSS: false, _publicPath: '/_nuxt/',
cssSourceMap: undefined,
ssr: undefined, additionalExtensions: [] as string[],
parallel: false, aggressiveCodeRemoval: false,
/**
* Use [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) to let you visualize your bundles and how to optimize them.
* @default false
*/
analyze: false as boolean | BundleAnalyzerPlugin.Options,
babel: {
configFile: false,
babelrc: false,
cacheDirectory: undefined
} as BabelOptions,
/**
* Enable cache of [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin#options) and [cache-loader](https://github.com/webpack-contrib/cache-loader#cache-loader)
*
* Experimental
* @default false
*/
cache: false, cache: false,
standalone: false, corejs: undefined as undefined | 'auto' | 2 | 3,
publicPath: '/_nuxt/', crossorigin: undefined as undefined | string,
serverURLPolyfill: 'url', /**
* Enables CSS Source Map support.
* @default true for dev and `false` for production
*/
cssSourceMap: undefined as undefined | boolean,
devMiddleware: {} as WebpackDevMiddlewareOptions,
devtools: undefined as undefined | boolean,
extend: null as null | ((
config: WebpackConfiguration,
ctx: {
loaders: Loaders
} & WebpackEnv
) => void),
/**
* Enables Common CSS Extraction using Vue Server Renderer guidelines.
*
* Using [extract-css-chunks-webpack-plugin](https://github.com/faceyspacey/extract-css-chunks-webpack-plugin/) under the hood, all your CSS will be extracted into separate files, usually one per component. This allows caching your CSS and JavaScript separately and is worth a try in case you have a lot of global or shared CSS.
*
* @default false
*/
extractCSS: false as boolean | Record<string, any>,
/**
* Customize bundle filenames.
*/
filenames: { filenames: {
// { isDev, isClient, isServer } app: ({ isDev, isModern }: WebpackEnv) => isDev ? `[name]${isModern ? '.modern' : ''}.js` : `[name].[contenthash:7]${isModern ? '.modern' : ''}.js`,
app: ({ isDev, isModern }) => isDev ? `[name]${isModern ? '.modern' : ''}.js` : `[name].[contenthash:7]${isModern ? '.modern' : ''}.js`, chunk: ({ isDev, isModern }: WebpackEnv) => isDev ? `[name]${isModern ? '.modern' : ''}.js` : `[name].[contenthash:7]${isModern ? '.modern' : ''}.js`,
chunk: ({ isDev, isModern }) => isDev ? `[name]${isModern ? '.modern' : ''}.js` : `[name].[contenthash:7]${isModern ? '.modern' : ''}.js`, css: ({ isDev }: WebpackEnv) => isDev ? '[name].css' : '[name].[contenthash:7].css',
css: ({ isDev }) => isDev ? '[name].css' : '[name].[contenthash:7].css', img: ({ isDev }: WebpackEnv) => isDev ? '[path][name].[ext]' : 'img/[name].[contenthash:7].[ext]',
img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[name].[contenthash:7].[ext]', font: ({ isDev }: WebpackEnv) => isDev ? '[path][name].[ext]' : 'fonts/[name].[contenthash:7].[ext]',
font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[name].[contenthash:7].[ext]', video: ({ isDev }: WebpackEnv) => isDev ? '[path][name].[ext]' : 'videos/[name].[contenthash:7].[ext]'
video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[name].[contenthash:7].[ext]'
}, },
/**
* By default, the build process does not scan files inside symlinks. This boolean includes them, thus allowing usage of symlinks inside folders such as the "pages" folder, for example.
* @default false
*/
followSymlinks: false,
/**
* Enables or disables the overlay provided by [FriendlyErrorsWebpackPlugin](https://github.com/nuxt/friendly-errors-webpack-plugin)
* @default true
*/
friendlyErrors: true,
hardSource: false,
hotMiddleware: {} as WebpackHotMiddlewareOptions & { client?: WebpackHotMiddlewareClientOptions },
html: {
/**
* Configuration for the [html-minifier plugin](https://github.com/kangax/html-minifier) used to minify HTML files created during the build process (will be applied for all modes).
*/
minify: {
collapseBooleanAttributes: true,
decodeEntities: true,
minifyCSS: true,
minifyJS: true,
processConditionalComments: true,
removeEmptyAttributes: true,
removeRedundantAttributes: true,
trimCustomFragments: true,
useShortDoctype: true
}
} as { minify: HtmlMinifierOptions },
indicator: {
position: 'bottom-right',
backgroundColor: '#2E495E',
color: '#00C48D'
} as boolean | { position: string, backgroundColor: string, color: string },
/**
* Customize options of Nuxt.js integrated webpack loaders.
*/
loaders: { loaders: {
/**
* Mor details at https://github.com/webpack-contrib/file-loader#options
*/
file: {}, file: {},
fontUrl: { limit: 1000 }, fontUrl: { limit: 1000 },
imgUrl: { limit: 1000 }, imgUrl: { limit: 1000 },
@ -47,18 +281,14 @@ export default () => ({
} }
}, },
scss: {}, scss: {},
// tODO
stylus: {}, stylus: {},
vueStyle: {} vueStyle: {}
}, } as Loaders,
styleResources: {}, loadingScreen: {} as Record<string, any> | false,
plugins: [],
terser: {},
hardSource: false,
aggressiveCodeRemoval: false,
optimizeCSS: undefined,
optimization: { optimization: {
runtimeChunk: 'single', runtimeChunk: 'single',
minimize: undefined, minimize: undefined as boolean | undefined,
minimizer: undefined, minimizer: undefined,
splitChunks: { splitChunks: {
chunks: 'all', chunks: 'all',
@ -69,62 +299,87 @@ export default () => ({
} }
} }
} }
}, } as WebpackConfiguration['optimization'],
splitChunks: { optimizeCSS: undefined as undefined | OptimizeCssAssetsWebpackPluginOptions | boolean,
layouts: false, /**
pages: true, * Enable [thread-loader](https://github.com/webpack-contrib/thread-loader#thread-loader) in webpack building
commons: true *
}, * Experimental
babel: { * @default false
configFile: false, */
babelrc: false, parallel: false,
cacheDirectory: undefined plugins: [] as WebpackPluginFunction[],
},
transpile: [], // Name of NPM packages to be transpiled
postcss: { postcss: {
preset: { preset: {
// https://cssdb.org/#staging-process // https://cssdb.org/#staging-process
stage: 2 stage: 2
} }
}, } as string[] | boolean | PostcssConfiguration | (() => PostcssConfiguration),
html: { /**
minify: { * Enable the profiler in [WebpackBar](https://github.com/nuxt/webpackbar#profile)
collapseBooleanAttributes: true, * @default false unless enabled by command line argument `--profile`
decodeEntities: true, */
minifyCSS: true, profile: process.argv.includes('--profile'),
minifyJS: true, /**
processConditionalComments: true, * Nuxt.js lets you upload your dist files to your CDN for maximum performances, simply set the `publicPath` to your CDN.
removeEmptyAttributes: true, * @default '/_nuxt/'
removeRedundantAttributes: true, * @example
trimCustomFragments: true, ```
useShortDoctype: true export default {
build: {
publicPath: 'https://cdn.nuxtjs.org'
}
} }
```
Then, when launching nuxt build, upload the content of .nuxt/dist/client directory to your CDN and voilà!
*/
publicPath: '/_nuxt/',
/**
* Suppresses most of the build output log
* @default true when a CI or test environment is detected by [std-env](https://github.com/nuxt-contrib/std-env)
*/
quiet: Boolean(env.ci || env.test),
/**
* @default 'url'
*/
serverURLPolyfill: 'url',
splitChunks: {
layouts: false,
pages: true,
commons: true
}, },
/**
template: undefined, * Creates special webpack bundle for SSR renderer.
templates: [], * @default true for universal mode and `false` for spa mode
*/
watch: [], ssr: undefined as undefined | boolean,
devMiddleware: {}, /**
hotMiddleware: {}, *
*/
standalone: false,
stats: { stats: {
excludeAssets: [ excludeAssets: [
/.map$/, /.map$/,
/index\..+\.html$/, /index\..+\.html$/,
/vue-ssr-(client|modern)-manifest.json/ /vue-ssr-(client|modern)-manifest.json/
] ]
}, } as 'none' | false | { excludeAssets: RegExp[] },
friendlyErrors: true, styleResources: {},
additionalExtensions: [], template: undefined,
warningIgnoreFilters: [], /**
* Nuxt.js allows you provide your own templates which will be rendered based on Nuxt configuration. This feature is specially useful for using with modules.
*/
templates: [] as Template[],
/**
* Terser plugin options. Set to `false` to disable this plugin. See https://github.com/webpack-contrib/terser-webpack-plugin
*/
terser: {} as TerserPluginOptions | boolean,
// Name of NPM packages to be transpiled
transpile: [] as Array<string | RegExp | ((context: WebpackEnv) => string | RegExp | undefined)>,
warningIgnoreFilters: [] as Array<(warn: Warning) => boolean>,
/**
* You can provide your custom files to watch and regenerate after changes. This feature is specially useful for using with modules.
*/
watch: [] as string[],
followSymlinks: false,
loadingScreen: {},
indicator: {
position: 'bottom-right',
backgroundColor: '#2E495E',
color: '#00C48D'
}
}) })

View File

@ -1,4 +1,11 @@
export default () => ({ import { Color } from 'chalk'
export interface CliOptions {
badgeMessages: string[]
bannerColor: typeof Color
}
export default (): CliOptions => ({
badgeMessages: [], badgeMessages: [],
bannerColor: 'green' bannerColor: 'green'
}) })

View File

@ -1,5 +1,33 @@
import { GlobbyOptions } from 'globby'
export default () => ({ type GenerateRoute = string | { route: string, payload: any }
type GenerateRoutesFunction = () => (Promise<GenerateRoute[]> | GenerateRoute[])
type GenerateRoutesFunctionWithCallback = (callback: (err: Error, routes: GenerateRoute[]) => void) => void
export interface GenerateOptions {
cache?: false | {
ignore?: string[] | Function,
globbyOptions?: GlobbyOptions
}
concurrency: number
crawler: boolean
devtools?: boolean
dir: string
exclude: RegExp[]
fallback: boolean | string
interval: number
routes: GenerateRoute[] | GenerateRoutesFunction | GenerateRoutesFunctionWithCallback
staticAssets: {
base?: string
versionBase?: string
dir?: string
version?: string
}
subFolders: boolean
}
export default (): GenerateOptions => ({
dir: 'dist', dir: 'dist',
routes: [], routes: [],
exclude: [], exclude: [],

View File

@ -9,16 +9,12 @@ import render from './render'
import router from './router' import router from './router'
import server from './server' import server from './server'
import cli from './cli' import cli from './cli'
import generate from './generate' import generate, { GenerateOptions } from './generate'
export const defaultNuxtConfigFile = 'nuxt.config' export const defaultNuxtConfigFile = 'nuxt.config'
export function getDefaultNuxtConfig (options = {}) { export const getDefaultNuxtConfig = () =>
if (!options.env) { ({
options.env = process.env
}
return {
..._app(), ..._app(),
..._common(), ..._common(),
build: build(), build: build(),
@ -26,8 +22,11 @@ export function getDefaultNuxtConfig (options = {}) {
modes: modes(), modes: modes(),
render: render(), render: render(),
router: router(), router: router(),
server: server(options), server: server({ env: process.env }) as ReturnType<typeof server> | boolean,
cli: cli(), cli: cli(),
generate: generate() generate: generate(),
} export: undefined as undefined | GenerateOptions,
} telemetry: undefined as undefined | boolean,
})
export type DefaultConfiguration = ReturnType<typeof getDefaultNuxtConfig>

View File

@ -8,7 +8,7 @@ export default () => ({
render: { render: {
ssr: true ssr: true
} }
}, } as const,
[MODES.spa]: { [MODES.spa]: {
build: { build: {
ssr: false ssr: false
@ -16,5 +16,5 @@ export default () => ({
render: { render: {
ssr: false ssr: false
} }
} } as const
}) })

View File

@ -1,6 +1,75 @@
// TODO: Refactor @nuxt/server related options into `server.js` // TODO: Refactor @nuxt/server related options into `server.js`
import type { ServerResponse } from 'http'
import type { Options as EtagOptions } from 'etag'
import type { IncomingMessage } from 'connect'
import type { CompressionOptions } from 'compression'
import type { ServeStaticOptions } from 'serve-static'
import type { ServerMiddleware } from './_common'
export default () => ({ interface PreloadFile {
asType: 'script' | 'style' | 'font'
extension: string
file: string
fileWithoutQuery: string
}
type ServePlaceholderHandler = 'default' | 'css' | 'html' | 'js' | 'json' | 'map' | 'plain' | 'image'
interface ServePlaceholderOptions {
handlers?: Record<string, ServePlaceholderHandler | null | false>
mimes?: Record<ServePlaceholderHandler, string | false | undefined>
noCache?: boolean
placeholders?: Record<ServePlaceholderHandler, string | Buffer | false>
skipUnknown?: boolean
statusCode?: false | number
}
type CspPolicyName = 'child-src' | 'connect-src' | 'default-src' | 'font-src' | 'frame-src' | 'img-src' | 'manifest-src' | 'media-src' | 'object-src' | 'prefetch-src' | 'script-src' | 'script-src-elem' | 'script-src-attr' | 'style-src' | 'style-src-elem' | 'style-src-attr' | 'worker-src' | 'base-uri' | 'plugin-types' | 'sandbox' | 'form-action' | 'frame-ancestors' | 'navigate-to' | 'report-uri' | 'report-to' | 'block-all-mixed-content' | 'referrer' | 'require-sri-for' | 'trusted-types' | 'upgrade-insecure-requests'
interface RenderOptions {
bundleRenderer: {
shouldPrefetch: () => boolean
shouldPreload: (fileWithoutQuery: string, asType: string) => boolean
runInNewContext?: boolean
}
compressor: CompressionOptions | ServerMiddleware | false
crossorigin?: 'anonymous' | 'use-credentials' | ''
csp: boolean | {
addMeta?: boolean
allowedSources?: string[]
hashAlgorithm?: string
policies?: Record<CspPolicyName, string[]>
reportOnly?: boolean
unsafeInlineCompatibility?: boolean
}
dist: ServeStaticOptions
etag: false | EtagOptions & {
hash?: (html: string) => string
}
fallback?: {
dist?: ServePlaceholderOptions
static?: ServePlaceholderOptions
}
/**
* @deprecated
*/
gzip?: CompressionOptions | ServerMiddleware | false
http2?: {
push?: boolean
shouldPush?: boolean | null
pushAssets?: null | ((
req: IncomingMessage,
res: ServerResponse,
publicPath: string,
preloadFiles: PreloadFile[]
) => string[])
}
resourceHints: boolean
ssr?: boolean
ssrLog?: boolean | 'collapsed'
static: ServeStaticOptions
}
export default (): RenderOptions => ({
bundleRenderer: { bundleRenderer: {
shouldPrefetch: () => false, shouldPrefetch: () => false,
shouldPreload: (fileWithoutQuery, asType) => ['script', 'style'].includes(asType), shouldPreload: (fileWithoutQuery, asType) => ['script', 'style'].includes(asType),
@ -15,9 +84,7 @@ export default () => ({
shouldPush: null, shouldPush: null,
pushAssets: null pushAssets: null
}, },
static: { static: {},
prefix: true
},
compressor: { compressor: {
threshold: 0 threshold: 0
}, },

View File

@ -1,4 +1,36 @@
export default () => ({ import { RouteRecordRaw, ScrollBehavior } from 'vue-router'
type UnionToIntersection<T> = (T extends any ? (k: T) => void : never) extends ((k: infer U) => void) ? U : never
type RouteConfig = UnionToIntersection<RouteRecordRaw>
export interface Route extends Pick<RouteConfig, Exclude<keyof RouteConfig, 'children' | 'component'>> {
children?: Route[]
chunkName?: string
chunkNames?: Record<string, string>
component?: RouteConfig['component'] | string
}
interface Middleware { }
export interface RouterConfigurationNormalized {
base: string
extendRoutes?(routes: Route[], resolve: (...pathSegments: string[]) => string): void
fallback: boolean
linkActiveClass: string | false
linkExactActiveClass: string | false
linkPrefetchedClass: string | false
middleware: Middleware[]
mode: 'history' | 'hash'
parseQuery: boolean
prefetchLinks: boolean
prefetchPayloads: boolean
routes: Route[]
routeNameSplitter: string
scrollBehavior: null | ScrollBehavior
stringifyQuery: boolean
trailingSlash?: boolean
}
export default (): RouterConfigurationNormalized => ({
mode: 'history', mode: 'history',
base: '/', base: '/',
routes: [], routes: [],

View File

@ -1,5 +1,19 @@
export default ({ env = {} } = {}) => ({ export interface ServerProcessEnv extends NodeJS.ProcessEnv {
https: false, NUXT_HOST?: string
NUXT_PORT?: string
HOST?: string
PORT?: string
UNIX_SOCKET?: string
npm_package_config_nuxt_port?: string
npm_package_config_nuxt_host?: string
npm_package_config_unix_socket?: string
}
export default ({ env = {} }: { env?: ServerProcessEnv } = {}) => ({
https: false as false | {
cert?: string | Buffer
key?: string | Buffer
},
port: env.NUXT_PORT || port: env.NUXT_PORT ||
env.PORT || env.PORT ||
env.npm_package_config_nuxt_port || env.npm_package_config_nuxt_port ||
@ -10,5 +24,5 @@ export default ({ env = {} } = {}) => ({
'localhost', 'localhost',
socket: env.UNIX_SOCKET || socket: env.UNIX_SOCKET ||
env.npm_package_config_unix_socket, env.npm_package_config_unix_socket,
timing: false timing: false as false | { total: boolean }
}) })

View File

@ -1,3 +1,3 @@
export { defaultNuxtConfigFile, getDefaultNuxtConfig } from './config' export { defaultNuxtConfigFile, getDefaultNuxtConfig } from './config'
export { getNuxtConfig } from './options' export { getNuxtConfig, Configuration, NormalizedConfiguration } from './options'
export { loadNuxtConfig } from './load' export { loadNuxtConfig } from './load'

View File

@ -11,10 +11,15 @@ import * as rc from 'rc9'
import { LoadOptions } from 'nuxt/core/load' import { LoadOptions } from 'nuxt/core/load'
import { defaultNuxtConfigFile } from './config' import { defaultNuxtConfigFile } from './config'
import { CliConfiguration, Configuration } from 'nuxt/config/options'
// @ts-ignore // @ts-ignore
const isJest = typeof jest !== 'undefined' const isJest = typeof jest !== 'undefined'
const interopDefault = (obj: any) => {
return 'default' in obj ? obj.default : obj
}
export interface EnvConfig { export interface EnvConfig {
dotenv?: string dotenv?: string
env?: NodeJS.ProcessEnv & { _applied?: boolean } env?: NodeJS.ProcessEnv & { _applied?: boolean }
@ -31,7 +36,7 @@ export async function loadNuxtConfig ({
}: LoadOptions = {}) { }: LoadOptions = {}) {
rootDir = path.resolve(rootDir) rootDir = path.resolve(rootDir)
let options = {} let options: CliConfiguration = {}
try { try {
configFile = require.resolve(path.resolve(rootDir, configFile)) configFile = require.resolve(path.resolve(rootDir, configFile))
@ -66,17 +71,11 @@ export async function loadNuxtConfig ({
// Clear cache // Clear cache
clearRequireCache(configFile) clearRequireCache(configFile)
const _require = createRequire(module) const _require = createRequire(module)
options = _require(configFile) || {} let _config: Configuration | ((ctx: Record<string, any>) => Promise<Configuration>) = interopDefault(_require(configFile) || {})
if (options.default) {
options = options.default
}
if (typeof options === 'function') { if (typeof _config === 'function') {
try { try {
options = await options(configContext) options = interopDefault(await _config(configContext))
if (options.default) {
options = options.default
}
} catch (error) { } catch (error) {
consola.error(error) consola.error(error)
consola.fatal('Error while fetching async configuration') consola.fatal('Error while fetching async configuration')

View File

@ -1,97 +1,104 @@
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import defaultsDeep from 'lodash/defaultsDeep' import consola from 'consola'
import defu from 'defu' import defu from 'defu'
import destr from 'destr'
import defaultsDeep from 'lodash/defaultsDeep'
import pick from 'lodash/pick' import pick from 'lodash/pick'
import uniq from 'lodash/uniq' import uniq from 'lodash/uniq'
import consola from 'consola'
import destr from 'destr'
import { TARGETS, MODES, guardDir, isNonEmptyString, isPureObject, isUrl, getMainModule, urlJoin, getPKG } from 'nuxt/utils'
import { defaultNuxtConfigFile, getDefaultNuxtConfig } from './config'
export function getNuxtConfig (_options) { import { TARGETS, MODES, guardDir, isNonEmptyString, isPureObject, isUrl, getMainModule, urlJoin, getPKG, Target, Mode } from 'nuxt/utils'
import { DefaultConfiguration, defaultNuxtConfigFile, getDefaultNuxtConfig } from './config'
import { deleteProp, mergeConfigs, setProp, overrideProp, Optional } from './transformers'
import type { EnvConfig } from 'nuxt/config/load'
interface InputConfiguration {
appTemplatePath?: string
layoutTransition?: string | DefaultConfiguration['layoutTransition']
loading?: true | false | DefaultConfiguration['loading']
manifest?: {
theme_color?: string
}
pageTransition?: string | DefaultConfiguration['pageTransition']
rootDir?: string
store?: boolean
}
export interface Configuration extends InputConfiguration, Optional<Omit<DefaultConfiguration, keyof InputConfiguration>> { }
export interface CliConfiguration extends Configuration {
// cli
_build?: boolean
_cli?: boolean
_export?: boolean
_generate?: boolean
_start?: boolean
_ready?: boolean
_legacyGenerate?: boolean
_env?: NodeJS.ProcessEnv
_envConfig?: EnvConfig
_nuxtConfigFiles?: string[]
}
export function getNuxtConfig(_options: Configuration) {
// Prevent duplicate calls // Prevent duplicate calls
if (_options.__normalized__) { if ('__normalized__' in _options) {
return _options return _options
} }
return normalizeConfig(_options as CliConfiguration)
}
function normalizeConfig(_options: CliConfiguration) {
// Clone options to prevent unwanted side-effects // Clone options to prevent unwanted side-effects
const options = Object.assign({}, _options) const _config: CliConfiguration = Object.assign({}, _options)
options.__normalized__ = true
setProp(_config, '__normalized__', true as const)
// Normalize options // Normalize options
if (options.loading === true) { if (_config.loading === true) {
delete options.loading deleteProp(_config, 'loading')
} }
if ( setProp(_config, '_routerBaseSpecified', _config.router && typeof _config.router.base === 'string')
options.router &&
options.router.middleware && overrideProp(_config, 'pageTransition', typeof _config.pageTransition === 'string' ? { name: _config.pageTransition } : _config.pageTransition)
!Array.isArray(options.router.middleware) overrideProp(_config, 'layoutTransition', typeof _config.layoutTransition === 'string' ? { name: _config.layoutTransition } : _config.layoutTransition)
) {
options.router.middleware = [options.router.middleware] if (typeof _config.extensions === 'string') {
_config.extensions = [_config.extensions]
} }
if (options.router && typeof options.router.base === 'string') { overrideProp(_config, 'globalName',
options._routerBaseSpecified = true (isNonEmptyString(_config.globalName) && /^[a-zA-Z]+$/.test(_config.globalName))
} ? _config.globalName.toLowerCase()
// use `` for preventing replacing to nuxt-edge
// TODO: Remove for Nuxt 3 : 'nuxt'
// router.scrollBehavior -> app/router.scrollBehavior.js )
if (options.router && typeof options.router.scrollBehavior !== 'undefined') {
consola.warn('`router.scrollBehavior` property is deprecated in favor of using `~/app/router.scrollBehavior.js` file, learn more: https://nuxtjs.org/api/configuration-router#scrollbehavior')
}
// TODO: Remove for Nuxt 3
// transition -> pageTransition
if (typeof options.transition !== 'undefined') {
consola.warn('`transition` property is deprecated in favor of `pageTransition` and will be removed in Nuxt 3')
options.pageTransition = options.transition
delete options.transition
}
if (typeof options.pageTransition === 'string') {
options.pageTransition = { name: options.pageTransition }
}
if (typeof options.layoutTransition === 'string') {
options.layoutTransition = { name: options.layoutTransition }
}
if (typeof options.extensions === 'string') {
options.extensions = [options.extensions]
}
options.globalName = (isNonEmptyString(options.globalName) && /^[a-zA-Z]+$/.test(options.globalName))
? options.globalName.toLowerCase()
// use `` for preventing replacing to nuxt-edge
: 'nuxt'
// Resolve rootDir // Resolve rootDir
options.rootDir = isNonEmptyString(options.rootDir) ? path.resolve(options.rootDir) : process.cwd() overrideProp(_config, 'rootDir',
isNonEmptyString(_config.rootDir) ? path.resolve(_config.rootDir) : process.cwd()
)
// Apply defaults by ${buildDir}/dist/build.config.js // Apply defaults by ${buildDir}/dist/build.config.js
// TODO: Unsafe operation. // TODO: Unsafe operation.
// const buildDir = options.buildDir || defaults.buildDir // const buildDir = _config.buildDir || defaults.buildDir
// const buildConfig = resolve(options.rootDir, buildDir, 'build.config.js') // const buildConfig = resolve(_config.rootDir, buildDir, 'build.config.js')
// if (existsSync(buildConfig)) { // if (existsSync(buildConfig)) {
// defaultsDeep(options, require(buildConfig)) // defaultsDeep(_config, require(buildConfig))
// } // }
// Apply defaults
const nuxtConfig = getDefaultNuxtConfig()
nuxtConfig.build._publicPath = nuxtConfig.build.publicPath
// Fall back to default if publicPath is falsy // Fall back to default if publicPath is falsy
if (options.build && !options.build.publicPath) { if (_config.build && !_config.build.publicPath) {
options.build.publicPath = undefined _config.build.publicPath = undefined
} }
defaultsDeep(options, nuxtConfig) // Apply defaults
const options = mergeConfigs(_config, getDefaultNuxtConfig())
// Target // Target
options.target = options.target || 'server'
if (!Object.values(TARGETS).includes(options.target)) { if (!Object.values(TARGETS).includes(options.target)) {
consola.warn(`Unknown target: ${options.target}. Falling back to server`) consola.warn(`Unknown target: ${options.target}. Falling back to server`)
options.target = 'server' options.target = 'server'
@ -127,16 +134,16 @@ export function getNuxtConfig (_options) {
const hasGenerateDir = isNonEmptyString(options.generate.dir) const hasGenerateDir = isNonEmptyString(options.generate.dir)
// Resolve srcDir // Resolve srcDir
options.srcDir = hasSrcDir overrideProp(options, 'srcDir', hasSrcDir
? path.resolve(options.rootDir, options.srcDir) ? path.resolve(options.rootDir, options.srcDir)
: options.rootDir : options.rootDir)
// Resolve buildDir // Resolve buildDir
options.buildDir = path.resolve(options.rootDir, options.buildDir) overrideProp(options, 'buildDir', path.resolve(options.rootDir, options.buildDir))
// Aliases // Aliases
const { rootDir, srcDir, dir: { assets: assetsDir, static: staticDir } } = options const { rootDir, srcDir, dir: { assets: assetsDir, static: staticDir } } = options
options.alias = { overrideProp(options, 'alias', {
'~~': rootDir, '~~': rootDir,
'@@': rootDir, '@@': rootDir,
'~': srcDir, '~': srcDir,
@ -144,18 +151,14 @@ export function getNuxtConfig (_options) {
[assetsDir]: path.join(srcDir, assetsDir), [assetsDir]: path.join(srcDir, assetsDir),
[staticDir]: path.join(srcDir, staticDir), [staticDir]: path.join(srcDir, staticDir),
...options.alias ...options.alias
} })
// Default value for _nuxtConfigFile // Default value for _nuxtConfigFile
if (!options._nuxtConfigFile) { overrideProp(options, '_nuxtConfigFile', options._nuxtConfigFile || path.resolve(options.rootDir, `${defaultNuxtConfigFile}.js`))
options._nuxtConfigFile = path.resolve(options.rootDir, `${defaultNuxtConfigFile}.js`)
}
if (!options._nuxtConfigFiles) { setProp(options, '_nuxtConfigFiles', (options as any)._nuxtConfigFiles || [
options._nuxtConfigFiles = [ options._nuxtConfigFile
options._nuxtConfigFile ])
]
}
// Watch for config file changes // Watch for config file changes
options.watch.push(...options._nuxtConfigFiles) options.watch.push(...options._nuxtConfigFiles)
@ -190,9 +193,9 @@ export function getNuxtConfig (_options) {
const mandatoryExtensions = ['js', 'mjs'] const mandatoryExtensions = ['js', 'mjs']
options.extensions = mandatoryExtensions overrideProp(options, 'extensions', mandatoryExtensions
.filter(ext => !options.extensions.includes(ext)) .filter(ext => !options.extensions.includes(ext))
.concat(options.extensions) .concat(options.extensions))
// If app.html is defined, set the template path to the user template // If app.html is defined, set the template path to the user template
if (options.appTemplatePath === undefined) { if (options.appTemplatePath === undefined) {
@ -204,8 +207,8 @@ export function getNuxtConfig (_options) {
options.appTemplatePath = path.resolve(options.srcDir, options.appTemplatePath) options.appTemplatePath = path.resolve(options.srcDir, options.appTemplatePath)
} }
options.build.publicPath = options.build.publicPath.replace(/([^/])$/, '$1/') overrideProp(options.build, 'publicPath', options.build.publicPath.replace(/([^/])$/, '$1/'))
options.build._publicPath = options.build._publicPath.replace(/([^/])$/, '$1/') overrideProp(options.build, '_publicPath', options.build._publicPath.replace(/([^/])$/, '$1/'))
// Ignore publicPath on dev // Ignore publicPath on dev
if (options.dev && isUrl(options.build.publicPath)) { if (options.dev && isUrl(options.build.publicPath)) {
@ -233,7 +236,7 @@ export function getNuxtConfig (_options) {
options.loadingIndicator = Object.assign( options.loadingIndicator = Object.assign(
{ {
name: 'default', name: 'default',
color: (options.loading && options.loading.color) || '#D3D3D3', color: (options.loading && typeof options.loading !== 'string' && typeof options.loading !== 'boolean' && options.loading.color) || '#D3D3D3',
color2: '#F5F5F5', color2: '#F5F5F5',
background: (options.manifest && options.manifest.theme_color) || 'white', background: (options.manifest && options.manifest.theme_color) || 'white',
dev: options.dev, dev: options.dev,
@ -244,15 +247,13 @@ export function getNuxtConfig (_options) {
} }
// Debug errors // Debug errors
if (options.debug === undefined) { overrideProp(options, 'debug', options.debug ?? options.dev)
options.debug = options.dev
}
// Validate that etag.hash is a function, if not unset it // Validate that etag.hash is a function, if not unset it
if (options.render.etag) { if (options.render.etag) {
const { hash } = options.render.etag const { hash } = options.render.etag
if (hash) { if (hash) {
const isFn = typeof hash === 'function' const isFn = hash instanceof Function
if (!isFn) { if (!isFn) {
options.render.etag.hash = undefined options.render.etag.hash = undefined
@ -273,64 +274,26 @@ export function getNuxtConfig (_options) {
unsafeInlineCompatibility: false, unsafeInlineCompatibility: false,
reportOnly: options.debug reportOnly: options.debug
}) })
// TODO: Remove this if statement in Nuxt 3, we will stop supporting this typo (more on: https://github.com/nuxt/nuxt.js/pull/6583)
if (options.render.csp.unsafeInlineCompatiblity) {
consola.warn('Using `unsafeInlineCompatiblity` is deprecated and will be removed in Nuxt 3. Use `unsafeInlineCompatibility` instead.')
options.render.csp.unsafeInlineCompatibility = options.render.csp.unsafeInlineCompatiblity
delete options.render.csp.unsafeInlineCompatiblity
}
} }
// cssSourceMap // cssSourceMap
if (options.build.cssSourceMap === undefined) { overrideProp(options.build, 'cssSourceMap', options.build.cssSourceMap ?? options.dev)
options.build.cssSourceMap = options.dev
}
const babelConfig = options.build.babel
// babel cacheDirectory // babel cacheDirectory
if (babelConfig.cacheDirectory === undefined) { const babelConfig = options.build.babel
babelConfig.cacheDirectory = options.dev overrideProp(options.build.babel, 'cacheDirectory', babelConfig.cacheDirectory ?? options.dev)
}
// TODO: remove this warn in Nuxt 3
if (Array.isArray(babelConfig.presets)) {
const warnPreset = (presetName) => {
const oldPreset = '@nuxtjs/babel-preset-app'
const newPreset = '@nuxt/babel-preset-app'
if (presetName.includes(oldPreset)) {
presetName = presetName.replace(oldPreset, newPreset)
consola.warn('@nuxtjs/babel-preset-app has been deprecated, please use @nuxt/babel-preset-app.')
}
return presetName
}
babelConfig.presets = babelConfig.presets.map((preset) => {
const hasOptions = Array.isArray(preset)
if (hasOptions) {
preset[0] = warnPreset(preset[0])
} else if (typeof preset === 'string') {
preset = warnPreset(preset)
}
return preset
})
}
// Vue config // Vue config
const vueConfig = options.vue.config const vueConfig = options.vue.config
if (vueConfig.silent === undefined) { overrideProp(options.vue.config, 'performance', vueConfig.performance !== undefined ? vueConfig.performance : options.dev)
vueConfig.silent = !options.dev
}
if (vueConfig.performance === undefined) {
vueConfig.performance = options.dev
}
// merge custom env with variables // merge custom env with variables
const eligibleEnvVariables = pick(process.env, Object.keys(process.env).filter(k => k.startsWith('NUXT_ENV_'))) const eligibleEnvVariables = pick(process.env, Object.keys(process.env).filter(k => k.startsWith('NUXT_ENV_')))
Object.assign(options.env, eligibleEnvVariables) overrideProp(options, 'env', Object.assign(options.env, eligibleEnvVariables))
// Normalize ignore // Normalize ignore
options.ignore = options.ignore ? [].concat(options.ignore) : [] overrideProp(options, 'ignore', options.ignore ? Array.from(options.ignore) : [])
// Append ignorePrefix glob to ignore // Append ignorePrefix glob to ignore
if (typeof options.ignorePrefix === 'string') { if (typeof options.ignorePrefix === 'string') {
@ -349,24 +312,23 @@ export function getNuxtConfig (_options) {
options.pageTransition.appear = true options.pageTransition.appear = true
} }
options.render.ssrLog = options.dev overrideProp(options.render, 'ssrLog', options.dev
? options.render.ssrLog === undefined || options.render.ssrLog ? options.render.ssrLog === undefined || options.render.ssrLog
: false : false)
// We assume the SPA fallback path is 404.html (for GitHub Pages, Surge, etc.) // We assume the SPA fallback path is 404.html (for GitHub Pages, Surge, etc.)
if (options.generate.fallback === true) { overrideProp(options.generate, 'fallback', options.generate.fallback === true ? '404.html' : options.generate.fallback)
options.generate.fallback = '404.html'
}
if (options.build.stats === 'none' || options.build.quiet === true) { if (options.build.stats === 'none' || options.build.quiet === true) {
options.build.stats = false options.build.stats = false
} }
// Vendor backward compatibility with nuxt 1.x // @pi0 - surely this can go
if (typeof options.build.vendor !== 'undefined') { // // Vendor backward compatibility with nuxt 1.x
delete options.build.vendor // if (typeof options.build.vendor !== 'undefined') {
consola.warn('vendor has been deprecated due to webpack4 optimization') // delete options.build.vendor
} // consola.warn('vendor has been deprecated due to webpack4 optimization')
// }
// Disable CSS extraction due to incompatibility with thread-loader // Disable CSS extraction due to incompatibility with thread-loader
if (options.build.extractCSS && options.build.parallel) { if (options.build.extractCSS && options.build.parallel) {
@ -375,17 +337,10 @@ export function getNuxtConfig (_options) {
} }
// build.extractCSS.allChunks has no effect // build.extractCSS.allChunks has no effect
if (typeof options.build.extractCSS.allChunks !== 'undefined') { if (typeof options.build.extractCSS !== 'boolean' && typeof options.build.extractCSS.allChunks !== 'undefined') {
consola.warn('build.extractCSS.allChunks has no effect from v2.0.0. Please use build.optimization.splitChunks settings instead.') consola.warn('build.extractCSS.allChunks has no effect from v2.0.0. Please use build.optimization.splitChunks settings instead.')
} }
// devModules has been renamed to buildModules
if (typeof options.devModules !== 'undefined') {
consola.warn('`devModules` has been renamed to `buildModules` and will be removed in Nuxt 3.')
options.buildModules.push(...options.devModules)
delete options.devModules
}
// Enable minimize for production builds // Enable minimize for production builds
if (options.build.optimization.minimize === undefined) { if (options.build.optimization.minimize === undefined) {
options.build.optimization.minimize = !options.dev options.build.optimization.minimize = !options.dev
@ -397,11 +352,11 @@ export function getNuxtConfig (_options) {
} }
const { loaders } = options.build const { loaders } = options.build
const vueLoader = loaders.vue // const vueLoader = loaders.vue
if (vueLoader.productionMode === undefined) { // if (vueLoader.productionMode === undefined) {
vueLoader.productionMode = !options.dev // vueLoader.productionMode = !options.dev
} // }
const styleLoaders = [ const styleLoaders: Array<keyof typeof loaders> = [
'css', 'cssModules', 'less', 'css', 'cssModules', 'less',
'sass', 'scss', 'stylus', 'vueStyle' 'sass', 'scss', 'stylus', 'vueStyle'
] ]
@ -412,7 +367,7 @@ export function getNuxtConfig (_options) {
} }
} }
options.build.transpile = [].concat(options.build.transpile || []) overrideProp(options.build, 'transpile', Array.from(options.build.transpile || []))
if (options.build.quiet === true) { if (options.build.quiet === true) {
consola.level = 0 consola.level = 0
@ -420,32 +375,22 @@ export function getNuxtConfig (_options) {
// Use runInNewContext for dev mode by default // Use runInNewContext for dev mode by default
const { bundleRenderer } = options.render const { bundleRenderer } = options.render
if (typeof bundleRenderer.runInNewContext === 'undefined') { overrideProp(options.render.bundleRenderer, 'runInNewContext', bundleRenderer.runInNewContext ?? options.dev)
bundleRenderer.runInNewContext = options.dev
// const { timing } = options.server
if (options.server && typeof options.server !== 'boolean' && options.server.timing) {
overrideProp(options.server, 'timing', { total: true, ...options.server.timing })
} }
// TODO: Remove this if statement in Nuxt 3
if (options.build.crossorigin) {
consola.warn('Using `build.crossorigin` is deprecated and will be removed in Nuxt 3. Please use `render.crossorigin` instead.')
options.render.crossorigin = options.build.crossorigin
delete options.build.crossorigin
}
const { timing } = options.server overrideProp(options, 'serverMiddleware', Array.isArray(options.serverMiddleware) ? options.serverMiddleware : Object.entries(options.serverMiddleware)
if (timing) { .map(([path, handler]) => ({ path, handler }))
options.server.timing = { total: true, ...timing } )
}
if (isPureObject(options.serverMiddleware)) {
options.serverMiddleware = Object.entries(options.serverMiddleware)
.map(([path, handler]) => ({ path, handler }))
}
// Generate staticAssets // Generate staticAssets
const { staticAssets } = options.generate const { staticAssets } = options.generate
if (!staticAssets.version) { overrideProp(options.generate.staticAssets, 'version', options.generate.staticAssets.version || String(Math.round(Date.now() / 1000)))
staticAssets.version = String(Math.round(Date.now() / 1000))
}
if (!staticAssets.base) { if (!staticAssets.base) {
const publicPath = isUrl(options.build.publicPath) ? '' : options.build.publicPath // "/_nuxt" or custom CDN URL const publicPath = isUrl(options.build.publicPath) ? '' : options.build.publicPath // "/_nuxt" or custom CDN URL
staticAssets.base = urlJoin(publicPath, staticAssets.dir) staticAssets.base = urlJoin(publicPath, staticAssets.dir)
@ -492,3 +437,5 @@ export function getNuxtConfig (_options) {
return options return options
} }
export type NormalizedConfiguration = ReturnType<typeof normalizeConfig>

View File

@ -0,0 +1,38 @@
import { defaultsDeep } from 'lodash'
export type Optional<T> = {
-readonly [K in keyof T]?: T[K] extends Function ? T[K] : T[K] extends RegExp ? T[K] : T[K] extends Promise<any> ? T[K] : T[K] extends Array<infer R> ? Array<R> : Optional<T[K]>
}
export function setProp<O extends Record<string, any>, K extends string, V>(obj: O, key: K, value: V): asserts obj is O & { [key in K]: V } {
(obj as { [key in K]: V })[key] = value
}
type Override<O extends Record<string, any>, K extends keyof O, V> = K extends Array<infer A> ? {
[P in keyof O]: K extends P ? O[P] extends Array<infer T> ? Array<A & T> : K | O[P] : O[P]
} : O & { [key in K]: V }
export function overrideProp<O extends Record<string, any>, K extends keyof O, V>(obj: O, key: K, value: V): asserts obj is Override<O, K, V> {
(obj as { [key in K]: V })[key] = value
}
export function deleteProp<O extends Record<string, any>, K extends string>(obj: O, key: K): asserts obj is Exclude<O, K> {
delete obj[key]
}
type MergeArrays<S, T> = S extends Array<infer A1> ? T extends Array<infer A2> ? Array<A1 | A2> : T | Array<A1> : T | S
type MergeObjects<S extends Record<string, any>, T extends Record<string, any>> = Omit<S & T, keyof S & keyof T> & {
-readonly [K in keyof S & keyof T]: Merge<S[K], T[K]>
}
type Merge<S, T> = S extends Array<any> ? MergeArrays<S, T> : S extends Function ? S | T : S extends RegExp ? S | T : S extends Promise<any> ? S | T : T extends Function ? S | T : S extends Record<string, any> ? T extends Record<string, any> ? MergeObjects<S, T> : S | T : MergeArrays<S, T>
// let b: Merged<{ test: string, another: number[] }, { third: () => void, another: string[] }> = {} as any
// b.another
// let c = b
// c.
// T extends Array<infer A> ? Array<Merged<A, S>> : T
export function mergeConfigs<Dest extends Record<string, any>, Source extends Record<string, any>>(dest: Dest, source: Source): Merge<Dest, Source> {
return defaultsDeep(dest, source)
}

View File

@ -16,8 +16,8 @@ export interface LoadOptions {
rootDir?: string rootDir?: string
envConfig?: EnvConfig envConfig?: EnvConfig
configFile?: string configFile?: string
configContext?: {} configContext?: Record<string, any>,
configOverrides?: {}, configOverrides?: Record<string, any>,
createRequire?: (module: NodeJS.Module) => NodeJS.Require createRequire?: (module: NodeJS.Module) => NodeJS.Require
} }

View File

@ -3,26 +3,31 @@ import fs from 'fs'
import hash from 'hash-sum' import hash from 'hash-sum'
import consola from 'consola' import consola from 'consola'
import { chainFn, sequence } from 'nuxt/utils' import type { NormalizedConfiguration } from 'nuxt/config'
import { chainFn, Mode, sequence } from 'nuxt/utils'
import Nuxt from './nuxt' import Nuxt from './nuxt'
import type { NuxtModule, ModuleHandler } from 'nuxt/config/config/_common'
interface Module { interface TemplateInput {
filename?: string
fileName?: string
options?: Record<string, any>
src: string src: string
options: Record<string, any> ssr?: boolean
handler: () => any mode?: 'all' | 'server' | 'client'
}
interface Template {
} }
export default class ModuleContainer { export default class ModuleContainer {
nuxt: Nuxt nuxt: Nuxt
options: Nuxt['options'] options: Nuxt['options']
requiredModules: Record<string, Module> requiredModules: Record<string, {
src: string
options: Record<string, any>
handler: ModuleHandler
}>
constructor (nuxt: Nuxt) { constructor(nuxt: Nuxt) {
this.nuxt = nuxt this.nuxt = nuxt
this.options = nuxt.options this.options = nuxt.options
this.requiredModules = {} this.requiredModules = {}
@ -35,7 +40,7 @@ export default class ModuleContainer {
} }
} }
async ready () { async ready() {
// Call before hook // Call before hook
await this.nuxt.callHook('modules:before', this, this.options.modules) await this.nuxt.callHook('modules:before', this, this.options.modules)
@ -54,17 +59,17 @@ export default class ModuleContainer {
await this.nuxt.callHook('modules:done', this) await this.nuxt.callHook('modules:done', this)
} }
addVendor () { addVendor() {
consola.warn('addVendor has been deprecated due to webpack4 optimization') consola.warn('addVendor has been deprecated due to webpack4 optimization')
} }
addTemplate (template) { addTemplate(template: TemplateInput | string) {
if (!template) { if (!template) {
throw new Error('Invalid template: ' + JSON.stringify(template)) throw new Error('Invalid template: ' + JSON.stringify(template))
} }
// Validate & parse source // Validate & parse source
const src = template.src || template const src = typeof template === 'string' ? template : template.src
const srcPath = path.parse(src) const srcPath = path.parse(src)
if (typeof src !== 'string' || !fs.existsSync(src)) { if (typeof src !== 'string' || !fs.existsSync(src)) {
@ -72,14 +77,14 @@ export default class ModuleContainer {
} }
// Mostly for DX, some people prefers `filename` vs `fileName` // Mostly for DX, some people prefers `filename` vs `fileName`
const fileName = template.fileName || template.filename const fileName = typeof template === 'string' ? '' : template.fileName || template.filename
// Generate unique and human readable dst filename if not provided // Generate unique and human readable dst filename if not provided
const dst = fileName || `${path.basename(srcPath.dir)}.${srcPath.name}.${hash(src)}${srcPath.ext}` const dst = fileName || `${path.basename(srcPath.dir)}.${srcPath.name}.${hash(src)}${srcPath.ext}`
// Add to templates list // Add to templates list
const templateObj = { const templateObj = {
src, src,
dst, dst,
options: template.options options: typeof template === 'string' ? undefined : template.options
} }
this.options.build.templates.push(templateObj) this.options.build.templates.push(templateObj)
@ -87,7 +92,7 @@ export default class ModuleContainer {
return templateObj return templateObj
} }
addPlugin (template) { addPlugin(template: TemplateInput) {
const { dst } = this.addTemplate(template) const { dst } = this.addTemplate(template)
// Add to nuxt plugins // Add to nuxt plugins
@ -99,7 +104,7 @@ export default class ModuleContainer {
}) })
} }
addLayout (template, name: string) { addLayout(template: TemplateInput, name: string) {
const { dst, src } = this.addTemplate(template) const { dst, src } = this.addTemplate(template)
const layoutName = name || path.parse(src).name const layoutName = name || path.parse(src).name
const layout = this.options.layouts[layoutName] const layout = this.options.layouts[layoutName]
@ -117,34 +122,34 @@ export default class ModuleContainer {
} }
} }
addErrorLayout (dst: string) { addErrorLayout(dst: string) {
const relativeBuildDir = path.relative(this.options.rootDir, this.options.buildDir) const relativeBuildDir = path.relative(this.options.rootDir, this.options.buildDir)
this.options.ErrorPage = `~/${relativeBuildDir}/${dst}` this.options.ErrorPage = `~/${relativeBuildDir}/${dst}`
} }
addServerMiddleware (middleware) { addServerMiddleware(middleware: NormalizedConfiguration['serverMiddleware'][number]) {
this.options.serverMiddleware.push(middleware) this.options.serverMiddleware.push(middleware)
} }
extendBuild (fn) { extendBuild(fn: NormalizedConfiguration['build']['extend']) {
this.options.build.extend = chainFn(this.options.build.extend, fn) this.options.build.extend = chainFn(this.options.build.extend, fn)
} }
extendRoutes (fn) { extendRoutes(fn: NormalizedConfiguration['router']['extendRoutes']) {
this.options.router.extendRoutes = chainFn( this.options.router.extendRoutes = chainFn(
this.options.router.extendRoutes, this.options.router.extendRoutes,
fn fn
) )
} }
requireModule (moduleOpts: Module) { requireModule(moduleOpts: NuxtModule) {
return this.addModule(moduleOpts) return this.addModule(moduleOpts)
} }
async addModule (moduleOpts) { async addModule(moduleOpts: NuxtModule) {
let src let src: string | ModuleHandler
let options: Record<string, any> let options: Record<string, any>
let handler let handler: ModuleHandler | ModuleHandler & { meta: { name: string } }
// Type 1: String or Function // Type 1: String or Function
if (typeof moduleOpts === 'string' || typeof moduleOpts === 'function') { if (typeof moduleOpts === 'string' || typeof moduleOpts === 'function') {
@ -168,7 +173,7 @@ export default class ModuleContainer {
} }
// Resolve handler // Resolve handler
if (!handler) { if (!handler && typeof src === 'string') {
try { try {
handler = this.nuxt.resolver.requireModule(src, { useESM: true }) handler = this.nuxt.resolver.requireModule(src, { useESM: true })
} catch (error) { } catch (error) {
@ -207,18 +212,20 @@ export default class ModuleContainer {
} }
// Ensure module is required once // Ensure module is required once
const metaKey = handler.meta && handler.meta.name if ('meta' in handler && typeof src === 'string') {
const key = metaKey || src const metaKey = handler.meta && handler.meta.name
if (typeof key === 'string') { const key = metaKey || src
if (this.requiredModules[key]) { if (typeof key === 'string') {
if (!metaKey) { if (this.requiredModules[key]) {
// TODO: Skip with nuxt3 if (!metaKey) {
consola.warn('Modules should be only specified once:', key) // TODO: Skip with nuxt3
} else { consola.warn('Modules should be only specified once:', key)
return } else {
return
}
} }
this.requiredModules[key] = { src, options, handler: handler as ModuleHandler }
} }
this.requiredModules[key] = { src, options, handler }
} }
// Default module options to empty object // Default module options to empty object

View File

@ -4,7 +4,7 @@ import consola from 'consola'
import Hookable from 'hookable' import Hookable from 'hookable'
import { defineAlias } from 'nuxt/utils' import { defineAlias } from 'nuxt/utils'
import { getNuxtConfig } from 'nuxt/config' import { getNuxtConfig, Configuration, NormalizedConfiguration } from 'nuxt/config'
import { Server } from 'nuxt/server' import { Server } from 'nuxt/server'
import { version } from '../../package.json' import { version } from '../../package.json'
@ -12,11 +12,15 @@ import { version } from '../../package.json'
import ModuleContainer from './module' import ModuleContainer from './module'
import Resolver from './resolver' import Resolver from './resolver'
declare global {
var __NUXT_DEV__: boolean
}
export default class Nuxt extends Hookable { export default class Nuxt extends Hookable {
_ready?: Promise<this> _ready?: Promise<this>
_initCalled?: boolean _initCalled?: boolean
options: any options: NormalizedConfiguration
resolver: Resolver resolver: Resolver
moduleContainer: ModuleContainer moduleContainer: ModuleContainer
server?: Server server?: Server
@ -24,7 +28,7 @@ export default class Nuxt extends Hookable {
render?: Server['app'] render?: Server['app']
showReady?: () => void showReady?: () => void
constructor (options = {}) { constructor(options: Configuration = {}) {
super(consola) super(consola)
// Assign options and apply defaults // Assign options and apply defaults
@ -66,28 +70,28 @@ export default class Nuxt extends Hookable {
} }
} }
static get version () { static get version() {
return `v${version}` + (global.__NUXT_DEV__ ? '-development' : '') return `v${version}` + (global.__NUXT_DEV__ ? '-development' : '')
} }
ready () { ready() {
if (!this._ready) { if (!this._ready) {
this._ready = this._init() this._ready = this._init()
} }
return this._ready return this._ready
} }
async _init () { async _init() {
if (this._initCalled) { if (this._initCalled) {
return this return this
} }
this._initCalled = true this._initCalled = true
// Add hooks // Add hooks
if (isPlainObject(this.options.hooks)) { if (this.options.hooks instanceof Function) {
this.addHooks(this.options.hooks)
} else if (typeof this.options.hooks === 'function') {
this.options.hooks(this.hook) this.options.hooks(this.hook)
} else if (isPlainObject(this.options.hooks)) {
this.addHooks(this.options.hooks)
} }
// Await for modules // Await for modules
@ -104,7 +108,7 @@ export default class Nuxt extends Hookable {
return this return this
} }
_initServer () { _initServer() {
if (this.server) { if (this.server) {
return return
} }
@ -114,14 +118,11 @@ export default class Nuxt extends Hookable {
defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen']) defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen'])
} }
async close (callback?: () => any | Promise<any>) { async close(callback?: () => any | Promise<any>) {
await this.callHook('close', this) await this.callHook('close', this)
if (typeof callback === 'function') { if (typeof callback === 'function') {
await callback() await callback()
} }
// Deleting as no longer exists on `hookable`
// this.clearHooks()
} }
} }

View File

@ -31,7 +31,7 @@ export default class Generator {
routes: Array<{ route: string } & Record<string, any>> routes: Array<{ route: string } & Record<string, any>>
generatedRoutes: Set<string> generatedRoutes: Set<string>
constructor (nuxt: Nuxt, builder?: Builder) { constructor(nuxt: Nuxt, builder?: Builder) {
this.nuxt = nuxt this.nuxt = nuxt
this.options = nuxt.options this.options = nuxt.options
this.builder = builder this.builder = builder
@ -53,7 +53,7 @@ export default class Generator {
} }
} }
async generate ({ build = true, init = true } = {}) { async generate({ build = true, init = true } = {}) {
consola.debug('Initializing generator...') consola.debug('Initializing generator...')
await this.initiate({ build, init }) await this.initiate({ build, init })
@ -80,7 +80,7 @@ export default class Generator {
return { errors } return { errors }
} }
async initiate ({ build = true, init = true } = {}) { async initiate({ build = true, init = true } = {}) {
// Wait for nuxt be ready // Wait for nuxt be ready
await this.nuxt.ready() await this.nuxt.ready()
@ -119,7 +119,7 @@ export default class Generator {
} }
} }
async initRoutes (...args) { async initRoutes(...args) {
// Resolve config.generate.routes promises before generating the routes // Resolve config.generate.routes promises before generating the routes
let generateRoutes = [] let generateRoutes = []
if (this.options.router.mode !== 'hash') { if (this.options.router.mode !== 'hash') {
@ -150,7 +150,7 @@ export default class Generator {
return routes return routes
} }
shouldGenerateRoute (route) { shouldGenerateRoute(route) {
return this.options.generate.exclude.every((regex) => { return this.options.generate.exclude.every((regex) => {
if (typeof regex === 'string') { if (typeof regex === 'string') {
return regex !== route return regex !== route
@ -159,7 +159,7 @@ export default class Generator {
}) })
} }
getBuildConfig () { getBuildConfig() {
try { try {
return require(path.join(this.options.buildDir, 'nuxt/config.json')) return require(path.join(this.options.buildDir, 'nuxt/config.json'))
} catch (err) { } catch (err) {
@ -167,11 +167,11 @@ export default class Generator {
} }
} }
getAppRoutes () { getAppRoutes() {
return require(path.join(this.options.buildDir, 'routes.json')) return require(path.join(this.options.buildDir, 'routes.json'))
} }
async generateRoutes (routes) { async generateRoutes(routes) {
const errors = [] const errors = []
this.routes = [] this.routes = []
@ -204,7 +204,7 @@ export default class Generator {
return errors return errors
} }
_formatErrors (errors) { _formatErrors(errors) {
return errors return errors
.map(({ type, route, error }) => { .map(({ type, route, error }) => {
const isHandled = type === 'handled' const isHandled = type === 'handled'
@ -223,7 +223,7 @@ export default class Generator {
.join('\n') .join('\n')
} }
async afterGenerate () { async afterGenerate() {
const { fallback } = this.options.generate const { fallback } = this.options.generate
// Disable SPA fallback if value isn't a non-empty string // Disable SPA fallback if value isn't a non-empty string
@ -255,7 +255,7 @@ export default class Generator {
consola.success('Client-side fallback created: `' + fallback + '`') consola.success('Client-side fallback created: `' + fallback + '`')
} }
async initDist () { async initDist() {
// Clean destination folder // Clean destination folder
await fsExtra.emptyDir(this.distPath) await fsExtra.emptyDir(this.distPath)
@ -283,7 +283,7 @@ export default class Generator {
await this.nuxt.callHook('export:distCopied', this) await this.nuxt.callHook('export:distCopied', this)
} }
decorateWithPayloads (routes, generateRoutes) { decorateWithPayloads(routes, generateRoutes) {
const routeMap = {} const routeMap = {}
// Fill routeMap for known routes // Fill routeMap for known routes
routes.forEach((route) => { routes.forEach((route) => {
@ -301,7 +301,7 @@ export default class Generator {
return Object.values(routeMap) return Object.values(routeMap)
} }
async generateRoute ({ route, payload = {}, errors = [] }) { async generateRoute({ route, payload = {}, errors = [] }) {
let html let html
const pageErrors = [] const pageErrors = []
@ -320,7 +320,8 @@ export default class Generator {
try { try {
const renderContext = { const renderContext = {
payload, payload,
staticAssetsBase: this.staticAssetsBase staticAssetsBase: this.staticAssetsBase,
staticAssets: undefined
} }
const res = await this.nuxt.server.renderRoute(route, renderContext) const res = await this.nuxt.server.renderRoute(route, renderContext)
html = res.html html = res.html
@ -377,7 +378,7 @@ export default class Generator {
pageErrors.push({ type: 'unhandled', route, error: minifyErr }) pageErrors.push({ type: 'unhandled', route, error: minifyErr })
} }
let fileName let fileName: string
if (this.options.generate.subFolders) { if (this.options.generate.subFolders) {
fileName = path.join(route, path.sep, 'index.html') // /about -> /about/index.html fileName = path.join(route, path.sep, 'index.html') // /about -> /about/index.html
@ -414,16 +415,8 @@ export default class Generator {
return true return true
} }
minifyHtml (html) { minifyHtml(html: string) {
let minificationOptions = this.options.build.html.minify const minificationOptions = this.options.build.html.minify
// Legacy: Override minification options with generate.minify if present
// TODO: Remove in Nuxt version 3
if (typeof this.options.generate.minify !== 'undefined') {
minificationOptions = this.options.generate.minify
consola.warn('generate.minify has been deprecated and will be removed in the next major version.' +
' Use build.html.minify instead!')
}
if (!minificationOptions) { if (!minificationOptions) {
return html return html

View File

@ -1,4 +1,5 @@
import consola from 'consola' import consola from 'consola'
import { BaseOptions, DOMWindow, VirtualConsole } from 'jsdom'
import { DeterminedGlobals, timeout } from 'nuxt/utils' import { DeterminedGlobals, timeout } from 'nuxt/utils'
interface Options { interface Options {
@ -28,12 +29,12 @@ export default async function renderAndGetWindow (
throw e throw e
}) })
const options = Object.assign({ const options: BaseOptions = Object.assign({
// Load subresources (https://github.com/tmpvar/jsdom#loading-subresources) // Load subresources (https://github.com/tmpvar/jsdom#loading-subresources)
resources: 'usable', resources: 'usable' as const,
runScripts: 'dangerously', runScripts: 'dangerously' as const,
virtualConsole: true, virtualConsole: undefined,
beforeParse (window: Window) { beforeParse (window: DOMWindow) {
// Mock window.scrollTo // Mock window.scrollTo
window.scrollTo = () => {} window.scrollTo = () => {}
} }
@ -44,8 +45,8 @@ export default async function renderAndGetWindow (
} }
if (options.virtualConsole) { if (options.virtualConsole) {
if (options.virtualConsole === true) { if (options.virtualConsole === undefined) {
options.virtualConsole = new jsdom.VirtualConsole().sendTo(consola) options.virtualConsole = new jsdom.VirtualConsole().sendTo(consola as unknown as Console)
} }
// Throw error when window creation failed // Throw error when window creation failed
options.virtualConsole.on('jsdomError', jsdomErrHandler) options.virtualConsole.on('jsdomError', jsdomErrHandler)
@ -58,13 +59,13 @@ export default async function renderAndGetWindow (
if (!nuxtExists) { if (!nuxtExists) {
const error = new Error('Could not load the nuxt app') const error = new Error('Could not load the nuxt app')
error.body = window.document.body.innerHTML ;(error as any).body = window.document.body.innerHTML
window.close() window.close()
throw error throw error
} }
// Used by Nuxt.js to say when the components are loaded and the app ready // Used by Nuxt.js to say when the components are loaded and the app ready
await timeout(new Promise((resolve) => { await timeout(new Promise<DOMWindow>((resolve) => {
window[loadedCallback] = () => resolve(window) window[loadedCallback] = () => resolve(window)
}), loadingTimeout, `Components loading in renderAndGetWindow was not completed in ${loadingTimeout / 1000}s`) }), loadingTimeout, `Components loading in renderAndGetWindow was not completed in ${loadingTimeout / 1000}s`)

View File

@ -8,6 +8,16 @@ import pify from 'pify'
let RANDOM_PORT = '0' let RANDOM_PORT = '0'
interface ListenerOptions {
port: number | string
host: string
socket: string
https: boolean
app: any
dev: boolean
baseURL: string
}
export default class Listener { export default class Listener {
port: number | string port: number | string
host: string host: string
@ -22,7 +32,7 @@ export default class Listener {
server: null | http.Server server: null | http.Server
address: null address: null
url: null | string url: null | string
constructor ({ port, host, socket, https, app, dev, baseURL }) { constructor ({ port, host, socket, https, app, dev, baseURL }: ListenerOptions) {
// Options // Options
this.port = port this.port = port
this.host = host this.host = host

View File

@ -33,7 +33,7 @@ export default class Server {
nuxt: Nuxt nuxt: Nuxt
globals: DeterminedGlobals globals: DeterminedGlobals
options: Nuxt['options'] options: Nuxt['options']
publicPath: boolean publicPath: string
renderer: VueRenderer renderer: VueRenderer
resources: { resources: {
clientManifest?: Manifest clientManifest?: Manifest
@ -114,7 +114,7 @@ export default class Server {
} }
} }
if (this.options.server.timing) { if (typeof this.options.server !== 'boolean' && this.options.server.timing) {
this.useMiddleware(createTimingMiddleware(this.options.server.timing)) this.useMiddleware(createTimingMiddleware(this.options.server.timing))
} }

View File

@ -67,8 +67,8 @@ interface AliasOptions {
warn?: boolean warn?: boolean
} }
export function defineAlias ( export function defineAlias <O extends Record<string, any>>(
src: string, src: O,
target: Record<string, any>, target: Record<string, any>,
prop: string | string[], prop: string | string[],
opts: AliasOptions = {} opts: AliasOptions = {}

View File

@ -213,7 +213,7 @@ export const createRoutes = function createRoutes ({
} }
// Guard dir1 from dir2 which can be indiscriminately removed // Guard dir1 from dir2 which can be indiscriminately removed
export const guardDir = function guardDir (options: Record<string, unknown>, key1: string, key2: string) { export const guardDir = function guardDir (options: Record<string, any>, key1: string, key2: string) {
const dir1 = get(options, key1, false) as string const dir1 = get(options, key1, false) as string
const dir2 = get(options, key2, false) as string const dir2 = get(options, key2, false) as string

View File

@ -15,8 +15,8 @@ async function promiseFinally<T> (
return result return result
} }
export const timeout = function timeout ( export const timeout = function timeout <T>(
fn: () => any, fn: Promise<T>,
ms: number, ms: number,
msg: string msg: string
) { ) {