feat: typed nuxt (1)

This commit is contained in:
Daniel Roe 2020-07-31 00:40:16 +01:00
parent f9005bb300
commit 38e72f86c2
51 changed files with 676 additions and 270 deletions

View File

@ -15,6 +15,7 @@ import template from 'lodash/template'
import uniq from 'lodash/uniq' import uniq from 'lodash/uniq'
import uniqBy from 'lodash/uniqBy' import uniqBy from 'lodash/uniqBy'
import type { Nuxt } from 'nuxt/core'
import { BundleBuilder } from 'nuxt/webpack' import { BundleBuilder } from 'nuxt/webpack'
import vueAppTemplate from 'nuxt/vue-app/template' import vueAppTemplate from 'nuxt/vue-app/template'
@ -26,6 +27,7 @@ import {
determineGlobals, determineGlobals,
stripWhitespace, stripWhitespace,
isIndexFileAndFolder, isIndexFileAndFolder,
DeterminedGlobals,
scanRequireTree, scanRequireTree,
TARGETS, TARGETS,
isFullStatic isFullStatic
@ -34,10 +36,35 @@ import {
import Ignore from './ignore' import Ignore from './ignore'
import BuildContext from './context/build' import BuildContext from './context/build'
import TemplateContext from './context/template' import TemplateContext from './context/template'
import { RouteLocationRaw } from 'vue-router'
const glob = pify(Glob) const glob = pify(Glob)
export default class Builder { export default class Builder {
constructor (nuxt, bundleBuilder) { __closed?: boolean
_buildStatus: typeof STATUS[keyof typeof STATUS]
_defaultPage?: boolean
_nuxtPages?: boolean
appFiles: string[]
bundleBuilder: BundleBuilder
globals: DeterminedGlobals
ignore: Ignore
nuxt: Nuxt
options: Nuxt['options']
plugins: Array<{
src: string
}>
relativeToBuild: (...args: string[]) => string
routes: RouteLocationRaw[]
supportedExtensions: string[]
template: typeof vueAppTemplate
watchers: {
files: null
custom: null
restart: null
}
constructor (nuxt: Nuxt) {
this.nuxt = nuxt this.nuxt = nuxt
this.plugins = [] this.plugins = []
this.options = nuxt.options this.options = nuxt.options
@ -51,7 +78,7 @@ export default class Builder {
this.supportedExtensions = ['vue', 'js', ...(this.options.build.additionalExtensions || [])] this.supportedExtensions = ['vue', 'js', ...(this.options.build.additionalExtensions || [])]
// Helper to resolve build paths // Helper to resolve build paths
this.relativeToBuild = (...args) => relativeTo(this.options.buildDir, ...args) this.relativeToBuild = (...args: string[]) => relativeTo(this.options.buildDir, ...args)
this._buildStatus = STATUS.INITIAL this._buildStatus = STATUS.INITIAL
@ -81,7 +108,7 @@ export default class Builder {
this.template = vueAppTemplate this.template = vueAppTemplate
// Create a new bundle builder // Create a new bundle builder
this.bundleBuilder = this.getBundleBuilder(bundleBuilder) this.bundleBuilder = this.getBundleBuilder()
this.ignore = new Ignore({ this.ignore = new Ignore({
rootDir: this.options.srcDir, rootDir: this.options.srcDir,
@ -846,4 +873,4 @@ const STATUS = {
INITIAL: 1, INITIAL: 1,
BUILD_DONE: 2, BUILD_DONE: 2,
BUILDING: 3 BUILDING: 3
} } as const

View File

@ -1,5 +1,12 @@
import type Builder from '../builder'
export default class BuildContext { export default class BuildContext {
constructor (builder) { _builder: Builder
nuxt: Builder['nuxt']
options: Builder['nuxt']['options']
target: Builder['nuxt']['options']['target']
constructor (builder: Builder) {
this._builder = builder this._builder = builder
this.nuxt = builder.nuxt this.nuxt = builder.nuxt
this.options = builder.nuxt.options this.options = builder.nuxt.options

View File

@ -1,13 +1,17 @@
import hash from 'hash-sum' import hash from 'hash-sum'
import consola from 'consola'
import uniqBy from 'lodash/uniqBy' 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 { r, wp, wChunk, serializeFunction, isFullStatic } from 'nuxt/utils' import { r, wp, wChunk, serializeFunction, isFullStatic } from 'nuxt/utils'
import type Builder from '../builder'
export default class TemplateContext { export default class TemplateContext {
constructor(builder, options) { templateFiles: string[]
templateVars: any
constructor (builder: Builder, options) {
this.templateFiles = Array.from(builder.template.files) this.templateFiles = Array.from(builder.template.files)
this.templateVars = { this.templateVars = {
nuxtOptions: options, nuxtOptions: options,
@ -53,7 +57,7 @@ export default class TemplateContext {
} }
} }
get templateOptions () { get templateOptions() {
return { return {
imports: { imports: {
serialize, serialize,
@ -62,7 +66,7 @@ export default class TemplateContext {
hash, hash,
r, r,
wp, wp,
wChunk, wChunk
}, },
interpolate: /<%=([\s\S]+?)%>/g interpolate: /<%=([\s\S]+?)%>/g
} }

View File

@ -2,7 +2,16 @@ import path from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
import ignore from 'ignore' import ignore from 'ignore'
type IgnoreInstance = ReturnType<typeof ignore>
type IgnoreOptions = Parameters<typeof ignore>[0]
export default class Ignore { export default class Ignore {
rootDir: string
ignore?: IgnoreInstance
ignoreArray?: string | string
ignoreFile?: string
ignoreOptions?: IgnoreOptions
constructor (options) { constructor (options) {
this.rootDir = options.rootDir this.rootDir = options.rootDir
this.ignoreOptions = options.ignoreOptions this.ignoreOptions = options.ignoreOptions
@ -44,7 +53,7 @@ export default class Ignore {
} }
} }
filter (paths) { filter (paths: string[]) {
if (this.ignore) { if (this.ignore) {
return this.ignore.filter([].concat(paths || [])) return this.ignore.filter([].concat(paths || []))
} }

View File

@ -1,10 +1,12 @@
import type { Nuxt } from 'nuxt/core'
import Builder from './builder' import Builder from './builder'
export { default as Builder } from './builder' export { default as Builder } from './builder'
export function getBuilder (nuxt) { export function getBuilder (nuxt: Nuxt) {
return new Builder(nuxt) return new Builder(nuxt)
} }
export function build (nuxt) { export function build (nuxt: Nuxt) {
return getBuilder(nuxt).build() return getBuilder(nuxt).build()
} }

View File

@ -1,43 +1,74 @@
import path from 'path' import path from 'path'
import consola from 'consola' import consola from 'consola'
import minimist from 'minimist' import minimist, { ParsedArgs } from 'minimist'
import Hookable from 'hable' import Hookable from 'hookable'
import { Nuxt } from 'nuxt/core'
import { Builder } from 'nuxt/builder'
import { Generator } from 'nuxt/generator'
import type { Target } from 'nuxt/utils'
import { name, version } from '../../package.json' import { name, version } from '../../package.json'
import { forceExit } from './utils' import { forceExit } from './utils'
import { loadNuxtConfig } from './utils/config' import { loadNuxtConfig } from './utils/config'
import { indent, foldLines, colorize } from './utils/formatting' import { indent, foldLines, colorize } from './utils/formatting'
import { startSpaces, optionSpaces, forceExitTimeout } from './utils/constants' import { startSpaces, optionSpaces, forceExitTimeout } from './utils/constants'
import { Nuxt } from 'nuxt/core'
import { Builder } from 'nuxt/builder' export interface Command {
import { Generator } from 'nuxt/generator' name: string
usage: string
description: string
options?: Record<string, any>
run?: (nuxt: NuxtCommand) => any | Promise<any>
}
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 {
constructor (cmd = { name: '', usage: '', description: '' }, argv = process.argv.slice(2), hooks = {}) { _argv: string[]
_parsedArgv: null | ParsedArgs
_lockRelease?: () => Promise<any>
cmd: Command & { options: Command['options'] }
constructor(cmd: Command = { name: '', usage: '', description: '' }, argv = process.argv.slice(2), hooks: Hooks = {}) {
super(consola) super(consola)
this.addHooks(hooks) this.addHooks(hooks)
if (!cmd.options) { if (!cmd.options) {
cmd.options = {} cmd.options = {}
} }
this.cmd = cmd this.cmd = cmd as Command & { options: Command['options'] }
this._argv = Array.from(argv) this._argv = Array.from(argv)
this._parsedArgv = null // Lazy evaluate this._parsedArgv = null // Lazy evaluate
} }
static run (cmd, argv, hooks) { static run(cmd: Command, argv: NodeJS.Process['argv'], hooks: Hooks) {
return NuxtCommand.from(cmd, argv, hooks).run() return NuxtCommand.from(cmd, argv, hooks).run()
} }
static from (cmd, argv, hooks) { static from(cmd: Command, argv: NodeJS.Process['argv'], hooks: Hooks) {
if (cmd instanceof NuxtCommand) { if (cmd instanceof NuxtCommand) {
return cmd return cmd
} }
return new NuxtCommand(cmd, argv, hooks) return new NuxtCommand(cmd, argv, hooks)
} }
async run () { async run() {
await this.callHook('run:before', { await this.callHook('run:before', {
argv: this._argv, argv: this._argv,
cmd: this.cmd, cmd: this.cmd,
@ -54,11 +85,11 @@ export default class NuxtCommand extends Hookable {
return return
} }
if (typeof this.cmd.run !== 'function') { if (!(this.cmd.run instanceof Function)) {
throw new TypeError('Invalid command! Commands should at least implement run() function.') throw new TypeError('Invalid command! Commands should at least implement run() function.')
} }
let cmdError let cmdError: any
try { try {
await this.cmd.run(this) await this.cmd.run(this)
@ -86,15 +117,15 @@ export default class NuxtCommand extends Hookable {
} }
} }
showVersion () { showVersion() {
process.stdout.write(`${name} v${version}\n`) process.stdout.write(`${name} v${version}\n`)
} }
showHelp () { showHelp() {
process.stdout.write(this._getHelp()) process.stdout.write(this._getHelp())
} }
get argv () { get argv() {
if (!this._parsedArgv) { if (!this._parsedArgv) {
const minimistOptions = this._getMinimistOptions() const minimistOptions = this._getMinimistOptions()
this._parsedArgv = minimist(this._argv, minimistOptions) this._parsedArgv = minimist(this._argv, minimistOptions)
@ -102,7 +133,7 @@ export default class NuxtCommand extends Hookable {
return this._parsedArgv return this._parsedArgv
} }
async getNuxtConfig (extraOptions = {}) { async getNuxtConfig(extraOptions: ExtraOptions = {}) {
// 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
@ -123,7 +154,7 @@ export default class NuxtCommand extends Hookable {
return options return options
} }
async getNuxt (options) { async getNuxt(options) {
const nuxt = new Nuxt(options) const nuxt = new Nuxt(options)
await nuxt.ready() await nuxt.ready()
@ -131,16 +162,16 @@ export default class NuxtCommand extends Hookable {
return nuxt return nuxt
} }
async getBuilder (nuxt) { async getBuilder(nuxt: Nuxt) {
return new Builder(nuxt) return new Builder(nuxt)
} }
async getGenerator (nuxt) { async getGenerator(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) {
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`)
@ -153,22 +184,22 @@ export default class NuxtCommand extends Hookable {
} }
} }
async releaseLock () { async releaseLock() {
if (this._lockRelease) { if (this._lockRelease) {
await this._lockRelease() await this._lockRelease()
this._lockRelease = undefined this._lockRelease = undefined
} }
} }
isUserSuppliedArg (option) { isUserSuppliedArg(option: string) {
return this._argv.includes(`--${option}`) || this._argv.includes(`--no-${option}`) return this._argv.includes(`--${option}`) || this._argv.includes(`--no-${option}`)
} }
_getDefaultOptionValue (option) { _getDefaultOptionValue<T, Option extends { default: ((cmd: Command) => T) | T }>(option: Option) {
return typeof option.default === 'function' ? option.default(this.cmd) : option.default return option.default instanceof Function ? option.default(this.cmd) : option.default
} }
_getMinimistOptions () { _getMinimistOptions() {
const minimistOptions = { const minimistOptions = {
alias: {}, alias: {},
boolean: [], boolean: [],
@ -193,7 +224,7 @@ export default class NuxtCommand extends Hookable {
return minimistOptions return minimistOptions
} }
_getHelp () { _getHelp() {
const options = [] const options = []
let maxOptionLength = 0 let maxOptionLength = 0

View File

@ -1,5 +1,8 @@
import consola from 'consola' import consola from 'consola'
import type { ParsedArgs } from 'minimist'
import { MODES, TARGETS } from 'nuxt/utils' import { MODES, TARGETS } from 'nuxt/utils'
import NuxtCommand from '../command'
import { common, locking } from '../options' import { common, locking } from '../options'
import { createLock } from '../utils' import { createLock } from '../utils'
@ -14,7 +17,7 @@ export default {
alias: 'a', alias: 'a',
type: 'boolean', type: 'boolean',
description: 'Launch webpack-bundle-analyzer to optimize your bundles', description: 'Launch webpack-bundle-analyzer to optimize your bundles',
prepare (cmd, options, argv) { prepare (cmd: NuxtCommand, options, argv: ParsedArgs) {
// Analyze option // Analyze option
options.build = options.build || {} options.build = options.build || {}
if (argv.analyze && typeof options.build.analyze !== 'object') { if (argv.analyze && typeof options.build.analyze !== 'object') {
@ -26,7 +29,7 @@ export default {
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Enable Vue devtools', description: 'Enable Vue devtools',
prepare (cmd, options, argv) { prepare (cmd: NuxtCommand, options, argv: ParsedArgs) {
options.vue = options.vue || {} options.vue = options.vue || {}
options.vue.config = options.vue.config || {} options.vue.config = options.vue.config || {}
if (argv.devtools) { if (argv.devtools) {
@ -43,7 +46,7 @@ export default {
alias: 'q', alias: 'q',
type: 'boolean', type: 'boolean',
description: 'Disable output except for errors', description: 'Disable output except for errors',
prepare (cmd, options, argv) { prepare (cmd: NuxtCommand, options, argv: ParsedArgs) {
// Silence output when using --quiet // Silence output when using --quiet
options.build = options.build || {} options.build = options.build || {}
if (argv.quiet) { if (argv.quiet) {
@ -55,14 +58,14 @@ export default {
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Bundle all server dependencies (useful for nuxt-start)', description: 'Bundle all server dependencies (useful for nuxt-start)',
prepare (cmd, options, argv) { prepare (cmd: NuxtCommand, options, argv: ParsedArgs) {
if (argv.standalone) { if (argv.standalone) {
options.build.standalone = true options.build.standalone = true
} }
} }
} }
}, },
async run (cmd) { async run (cmd: NuxtCommand) {
const config = await cmd.getNuxtConfig({ dev: false, server: false, _build: true }) const config = await cmd.getNuxtConfig({ dev: false, server: false, _build: true })
config.server = (config.mode === MODES.spa || config.ssr === false) && cmd.argv.generate !== false config.server = (config.mode === MODES.spa || config.ssr === false) && cmd.argv.generate !== false
const nuxt = await cmd.getNuxt(config) const nuxt = await cmd.getNuxt(config)

View File

@ -1,6 +1,11 @@
import consola from 'consola' import consola from 'consola'
import chalk from 'chalk' import chalk from 'chalk'
import opener from 'opener' import opener from 'opener'
import type { ParsedArgs } from 'minimist'
import { Nuxt } from 'nuxt/core'
import type NuxtCommand from '../command'
import { common, server } from '../options' import { common, server } from '../options'
import { eventsMapping, formatPath } from '../utils' import { eventsMapping, formatPath } from '../utils'
import { showBanner } from '../utils/banner' import { showBanner } from '../utils/banner'
@ -20,13 +25,13 @@ export default {
} }
}, },
async run (cmd) { async run (cmd: NuxtCommand) {
const { argv } = cmd const { argv } = cmd
await this.startDev(cmd, argv, argv.open) await this.startDev(cmd, argv, argv.open)
}, },
async startDev (cmd, argv) { async startDev (cmd: NuxtCommand, argv) {
let nuxt let nuxt
try { try {
nuxt = await this._listenDev(cmd, argv) nuxt = await this._listenDev(cmd, argv)
@ -45,7 +50,7 @@ export default {
return nuxt return nuxt
}, },
async _listenDev (cmd, argv) { async _listenDev (cmd: NuxtCommand, argv) {
const config = await cmd.getNuxtConfig({ dev: true, _build: true }) const config = await cmd.getNuxtConfig({ dev: true, _build: true })
const nuxt = await cmd.getNuxt(config) const nuxt = await cmd.getNuxt(config)
@ -73,7 +78,7 @@ export default {
return nuxt return nuxt
}, },
async _buildDev (cmd, argv, nuxt) { async _buildDev (cmd: NuxtCommand, argv: ParsedArgs, nuxt: Nuxt) {
// Create builder instance // Create builder instance
const builder = await cmd.getBuilder(nuxt) const builder = await cmd.getBuilder(nuxt)
@ -110,7 +115,7 @@ export default {
await this.startDev(cmd, argv) await this.startDev(cmd, argv)
}, },
onBundlerChange (path) { onBundlerChange (path: string) {
this.logChanged({ event: 'change', path }) this.logChanged({ event: 'change', path })
} }
} }

View File

@ -1,6 +1,7 @@
import path from 'path' import path from 'path'
import consola from 'consola' import consola from 'consola'
import { TARGETS } from 'nuxt/utils' import { TARGETS } from 'nuxt/utils'
import type NuxtCommand from '../command'
import { common, locking } from '../options' import { common, locking } from '../options'
import { createLock } from '../utils' import { createLock } from '../utils'
@ -17,7 +18,7 @@ export default {
description: 'Exit with non-zero status code if there are errors when exporting pages' description: 'Exit with non-zero status code if there are errors when exporting pages'
} }
}, },
async run (cmd) { async run (cmd: NuxtCommand) {
const config = await cmd.getNuxtConfig({ const config = await cmd.getNuxtConfig({
dev: false, dev: false,
target: TARGETS.static, target: TARGETS.static,

View File

@ -1,4 +1,6 @@
import type { ParsedArgs } from 'minimist'
import { TARGETS } from 'nuxt/utils' import { TARGETS } from 'nuxt/utils'
import type NuxtCommand from '../command'
import { common, locking } from '../options' import { common, locking } from '../options'
import { normalizeArg, createLock } from '../utils' import { normalizeArg, createLock } from '../utils'
@ -18,7 +20,7 @@ export default {
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Enable Vue devtools', description: 'Enable Vue devtools',
prepare (cmd, options, argv) { prepare (_cmd: NuxtCommand, options, argv: ParsedArgs) {
options.vue = options.vue || {} options.vue = options.vue || {}
options.vue.config = options.vue.config || {} options.vue.config = options.vue.config || {}
if (argv.devtools) { if (argv.devtools) {
@ -30,7 +32,7 @@ export default {
alias: 'q', alias: 'q',
type: 'boolean', type: 'boolean',
description: 'Disable output except for errors', description: 'Disable output except for errors',
prepare (cmd, options, argv) { prepare (_cmd: NuxtCommand, options, argv: ParsedArgs) {
// Silence output when using --quiet // Silence output when using --quiet
options.build = options.build || {} options.build = options.build || {}
if (argv.quiet) { if (argv.quiet) {
@ -41,7 +43,7 @@ export default {
modern: { modern: {
...common.modern, ...common.modern,
description: 'Generate app in modern build (modern mode can be only client)', description: 'Generate app in modern build (modern mode can be only client)',
prepare (cmd, options, argv) { prepare (_cmd: NuxtCommand, options, argv: ParsedArgs) {
if (normalizeArg(argv.modern)) { if (normalizeArg(argv.modern)) {
options.modern = 'client' options.modern = 'client'
} }
@ -53,7 +55,7 @@ export default {
description: 'Exit with non-zero status code if there are errors when generating pages' description: 'Exit with non-zero status code if there are errors when generating pages'
} }
}, },
async run (cmd) { async run (cmd: NuxtCommand) {
const config = await cmd.getNuxtConfig({ const config = await cmd.getNuxtConfig({
dev: false, dev: false,
_build: cmd.argv.build, _build: cmd.argv.build,

View File

@ -12,7 +12,7 @@ export default {
help: common.help, help: common.help,
version: common.version version: common.version
}, },
async run (cmd) { async run (cmd: NuxtCommand) {
const [name] = cmd._argv const [name] = cmd._argv
if (!name) { if (!name) {
return listCommands() return listCommands()

View File

@ -9,7 +9,7 @@ const _commands = {
help: () => import('./help') help: () => import('./help')
} }
export default function getCommand (name) { export default function getCommand (name: keyof typeof _commands) {
if (!_commands[name]) { if (!_commands[name]) {
return Promise.resolve(null) return Promise.resolve(null)
} }

View File

@ -5,10 +5,11 @@ import serveStatic from 'serve-static'
import compression from 'compression' import compression from 'compression'
import { getNuxtConfig } from 'nuxt/config' import { getNuxtConfig } from 'nuxt/config'
import { TARGETS } from 'nuxt/utils' import { TARGETS } from 'nuxt/utils'
import { common, server } from '../options'
import { showBanner } from '../utils/banner'
import { Listener } from 'nuxt/server' import { Listener } from 'nuxt/server'
import { Nuxt } from 'nuxt/core' import { Nuxt } from 'nuxt/core'
import type NuxtCommand from '../command'
import { common, server } from '../options'
import { showBanner } from '../utils/banner'
export default { export default {
name: 'serve', name: 'serve',
@ -20,7 +21,7 @@ export default {
help: common.help, help: common.help,
...server ...server
}, },
async run (cmd) { async run (cmd: NuxtCommand) {
let options = await cmd.getNuxtConfig({ dev: false }) let options = await cmd.getNuxtConfig({ dev: false })
// add default options // add default options
options = getNuxtConfig(options) options = getNuxtConfig(options)

View File

@ -1,4 +1,6 @@
import { TARGETS } from 'nuxt/utils' import { TARGETS } from 'nuxt/utils'
import type NuxtCommand from '../command'
import { common, server } from '../options' import { common, server } from '../options'
import { showBanner } from '../utils/banner' import { showBanner } from '../utils/banner'
@ -10,7 +12,7 @@ export default {
...common, ...common,
...server ...server
}, },
async run (cmd) { async run (cmd: NuxtCommand) {
const config = await cmd.getNuxtConfig({ dev: false, _start: true }) const config = await cmd.getNuxtConfig({ dev: false, _start: true })
if (config.target === TARGETS.static) { if (config.target === TARGETS.static) {
throw new Error('You cannot use `nuxt start` with ' + TARGETS.static + ' target, please use `nuxt export` and `nuxt serve`') throw new Error('You cannot use `nuxt start` with ' + TARGETS.static + ' target, please use `nuxt export` and `nuxt serve`')

View File

@ -1,6 +1,8 @@
import util from 'util' import util from 'util'
import consola from 'consola' import consola from 'consola'
import get from 'lodash/get' import get from 'lodash/get'
import type NuxtCommand from '../command'
import { common } from '../options' import { common } from '../options'
export default { export default {
@ -32,7 +34,7 @@ export default {
description: 'Inspect development mode webpack config' description: 'Inspect development mode webpack config'
} }
}, },
async run (cmd) { async run (cmd: NuxtCommand) {
const { name } = cmd.argv const { name } = cmd.argv
const queries = [...cmd.argv._] const queries = [...cmd.argv._]

View File

@ -4,7 +4,7 @@ import { startSpaces, optionSpaces } from './utils/constants'
import getCommand from './commands' import getCommand from './commands'
export default async function listCommands () { export default async function listCommands () {
const commandsOrder = ['dev', 'build', 'generate', 'start', 'help'] const commandsOrder = ['dev', 'build', 'generate', 'start', 'help'] as const
// Load all commands // Load all commands
const _commands = await Promise.all( const _commands = await Promise.all(

View File

@ -5,7 +5,7 @@ import NuxtCommand from './command'
import setup from './setup' import setup from './setup'
import getCommand from './commands' import getCommand from './commands'
function packageExists (name) { function packageExists (name: string) {
try { try {
require.resolve(name) require.resolve(name)
return true return true
@ -14,7 +14,7 @@ function packageExists (name) {
} }
} }
export default async function run(_argv, hooks = {}) { export default async function run (_argv: NodeJS.Process['argv'], hooks = {}) {
// Check for not installing both nuxt and nuxt-edge // Check for not installing both nuxt and nuxt-edge
const dupPkg = '@nuxt/' + (pkgName === '@nuxt/cli-edge' ? 'cli' : 'cli-edge') const dupPkg = '@nuxt/' + (pkgName === '@nuxt/cli-edge' ? 'cli' : 'cli-edge')
if (packageExists(dupPkg)) { if (packageExists(dupPkg)) {
@ -25,7 +25,7 @@ export default async function run(_argv, hooks = {}) {
const argv = _argv ? Array.from(_argv) : process.argv.slice(2) const argv = _argv ? Array.from(_argv) : process.argv.slice(2)
// Check for internal command // Check for internal command
let cmd = await getCommand(argv[0]) let cmd = await getCommand(argv[0] as any)
// Matching `nuxt` or `nuxt [dir]` or `nuxt -*` for `nuxt dev` shortcut // Matching `nuxt` or `nuxt [dir]` or `nuxt -*` for `nuxt dev` shortcut
if (!cmd && (!argv[0] || argv[0][0] === '-' || (argv[0] !== 'static' && fs.existsSync(argv[0])))) { if (!cmd && (!argv[0] || argv[0][0] === '-' || (argv[0] !== 'static' && fs.existsSync(argv[0])))) {

View File

@ -4,7 +4,7 @@ import { fatalBox } from './utils/formatting'
let _setup = false let _setup = false
export default function setup ({ dev }) { export default function setup ({ dev }: { dev: boolean }) {
// Apply default NODE_ENV if not provided // Apply default NODE_ENV if not provided
if (!process.env.NODE_ENV) { if (!process.env.NODE_ENV) {
process.env.NODE_ENV = dev ? 'development' : 'production' process.env.NODE_ENV = dev ? 'development' : 'production'

View File

@ -1,10 +1,12 @@
import consola from 'consola' import consola from 'consola'
import env from 'std-env' import env from 'std-env'
import chalk from 'chalk' import chalk from 'chalk'
import { Nuxt } from 'nuxt/core'
import { successBox } from './formatting' import { successBox } from './formatting'
import { getFormattedMemoryUsage } from './memory' import { getFormattedMemoryUsage } from './memory'
export function showBanner (nuxt, showMemoryUsage = true) { export function showBanner (nuxt: Nuxt, showMemoryUsage = true) {
if (env.test) { if (env.test) {
return return
} }
@ -23,7 +25,7 @@ export function showBanner (nuxt, showMemoryUsage = true) {
const { bannerColor, badgeMessages } = nuxt.options.cli const { bannerColor, badgeMessages } = nuxt.options.cli
titleLines.push(`${chalk[bannerColor].bold('Nuxt.js')} @ ${nuxt.constructor.version || 'exotic'}\n`) titleLines.push(`${chalk[bannerColor].bold('Nuxt.js')} @ ${nuxt.constructor.version || 'exotic'}\n`)
const label = name => chalk.bold.cyan(`${name}:`) const label = (name: string) => chalk.bold.cyan(`${name}:`)
// Environment // Environment
const isDev = nuxt.options.dev const isDev = nuxt.options.dev

View File

@ -1,9 +1,11 @@
import path from 'path' import path from 'path'
import defaultsDeep from 'lodash/defaultsDeep' import defaultsDeep from 'lodash/defaultsDeep'
import type { ParsedArgs } from 'minimist'
import { loadNuxtConfig as _loadNuxtConfig, getDefaultNuxtConfig } from 'nuxt/config' import { loadNuxtConfig as _loadNuxtConfig, getDefaultNuxtConfig } from 'nuxt/config'
import { MODES } from 'nuxt/utils' import { MODES } from 'nuxt/utils'
export async function loadNuxtConfig (argv, configContext) { export async function loadNuxtConfig (argv: ParsedArgs, configContext) {
const rootDir = path.resolve(argv._[0] || '.') const rootDir = path.resolve(argv._[0] || '.')
const configFile = argv['config-file'] const configFile = argv['config-file']

View File

@ -3,11 +3,11 @@ import chalk from 'chalk'
import boxen from 'boxen' import boxen from 'boxen'
import { maxCharsPerLine } from './constants' import { maxCharsPerLine } from './constants'
export function indent (count, chr = ' ') { export function indent (count: number, chr = ' ') {
return chr.repeat(count) return chr.repeat(count)
} }
export function indentLines (string, spaces, firstLineSpaces) { export function indentLines (string: string | string[], spaces: number, firstLineSpaces?: number) {
const lines = Array.isArray(string) ? string : string.split('\n') const lines = Array.isArray(string) ? string : string.split('\n')
let s = '' let s = ''
if (lines.length) { if (lines.length) {
@ -21,11 +21,11 @@ export function indentLines (string, spaces, firstLineSpaces) {
return s return s
} }
export function foldLines (string, spaces, firstLineSpaces, charsPerLine = maxCharsPerLine()) { export function foldLines (string: string, spaces: number, firstLineSpaces?: number, charsPerLine = maxCharsPerLine()) {
return indentLines(wrapAnsi(string, charsPerLine), spaces, firstLineSpaces) return indentLines(wrapAnsi(string, charsPerLine), spaces, firstLineSpaces)
} }
export function colorize (text) { export function colorize (text: string) {
return text return text
.replace(/\[[^ ]+]/g, m => chalk.grey(m)) .replace(/\[[^ ]+]/g, m => chalk.grey(m))
.replace(/<[^ ]+>/g, m => chalk.green(m)) .replace(/<[^ ]+>/g, m => chalk.green(m))
@ -33,7 +33,7 @@ export function colorize (text) {
.replace(/`([^`]+)`/g, (_, m) => chalk.bold.cyan(m)) .replace(/`([^`]+)`/g, (_, m) => chalk.bold.cyan(m))
} }
export function box (message, title, options) { export function box (message: string, title: string, options?: boxen.Options) {
return boxen([ return boxen([
title || chalk.white('Nuxt Message'), title || chalk.white('Nuxt Message'),
'', '',
@ -46,24 +46,24 @@ export function box (message, title, options) {
}, options)) + '\n' }, options)) + '\n'
} }
export function successBox (message, title) { export function successBox (message: string, title?: string) {
return box(message, title || chalk.green('✔ Nuxt Success'), { return box(message, title || chalk.green('✔ Nuxt Success'), {
borderColor: 'green' borderColor: 'green'
}) })
} }
export function warningBox (message, title) { export function warningBox (message: string, title?: string) {
return box(message, title || chalk.yellow('⚠ Nuxt Warning'), { return box(message, title || chalk.yellow('⚠ Nuxt Warning'), {
borderColor: 'yellow' borderColor: 'yellow'
}) })
} }
export function errorBox (message, title) { export function errorBox (message: string, title?: string) {
return box(message, title || chalk.red('✖ Nuxt Error'), { return box(message, title || chalk.red('✖ Nuxt Error'), {
borderColor: 'red' borderColor: 'red'
}) })
} }
export function fatalBox (message, title) { export function fatalBox (message: string, title?: string) {
return errorBox(message, title || chalk.red('✖ Nuxt Fatal Error')) return errorBox(message, title || chalk.red('✖ Nuxt Fatal Error'))
} }

View File

@ -12,7 +12,7 @@ export const eventsMapping = {
unlink: { icon: '-', color: 'red', action: 'Removed' } unlink: { icon: '-', color: 'red', action: 'Removed' }
} }
export function formatPath (filePath) { export function formatPath (filePath: string) {
if (!filePath) { if (!filePath) {
return return
} }
@ -27,7 +27,7 @@ export function formatPath (filePath) {
* @param {*} defaultValue * @param {*} defaultValue
* @returns formatted argument * @returns formatted argument
*/ */
export function normalizeArg (arg, defaultValue) { export function normalizeArg (arg: boolean | 'true' | '' | 'false', defaultValue?: boolean) {
switch (arg) { switch (arg) {
case 'true': arg = true; break case 'true': arg = true; break
case '': arg = true; break case '': arg = true; break
@ -37,7 +37,7 @@ export function normalizeArg (arg, defaultValue) {
return arg return arg
} }
export function forceExit (cmdName, timeout) { export function forceExit (cmdName: string, timeout: number | false) {
if (timeout !== false) { if (timeout !== false) {
const exitTimeout = setTimeout(() => { const exitTimeout = setTimeout(() => {
const msg = `The command 'nuxt ${cmdName}' finished but did not exit after ${timeout}s const msg = `The command 'nuxt ${cmdName}' finished but did not exit after ${timeout}s
@ -59,6 +59,6 @@ ${chalk.bold('DeprecationWarning: Starting with Nuxt version 3 this will be a fa
// An immediate export throws an error when mocking with jest // An immediate export throws an error when mocking with jest
// TypeError: Cannot set property createLock of #<Object> which has only a getter // TypeError: Cannot set property createLock of #<Object> which has only a getter
export function createLock (...args) { export function createLock (...args: Parameters<typeof lock>) {
return lock(...args) return lock(...args)
} }

View File

@ -1,7 +1,7 @@
import { loadNuxt } from 'nuxt/core' import { loadNuxt } from 'nuxt/core'
import { getBuilder } from 'nuxt/builder' import { getBuilder } from 'nuxt/builder'
export async function getWebpackConfig(name = 'client', loadOptions = {}) { export async function getWebpackConfig (name = 'client', loadOptions = {}) {
const nuxt = await loadNuxt(loadOptions) const nuxt = await loadNuxt(loadOptions)
const builder = await getBuilder(nuxt) const builder = await getBuilder(nuxt)
const { bundleBuilder } = builder const { bundleBuilder } = builder

View File

@ -8,18 +8,27 @@ import jiti from 'jiti'
import _createRequire from 'create-require' import _createRequire from 'create-require'
import destr from 'destr' import destr from 'destr'
import * as rc from 'rc9' import * as rc from 'rc9'
import { LoadOptions } from 'nuxt/core/load'
import { defaultNuxtConfigFile } from './config' import { defaultNuxtConfigFile } from './config'
// @ts-ignore
const isJest = typeof jest !== 'undefined' const isJest = typeof jest !== 'undefined'
export interface EnvConfig {
dotenv?: string
env?: NodeJS.ProcessEnv & { _applied?: boolean }
expand?: boolean
}
export async function loadNuxtConfig ({ export async function loadNuxtConfig ({
rootDir = '.', rootDir = '.',
envConfig = {}, envConfig = {},
configFile = defaultNuxtConfigFile, configFile = defaultNuxtConfigFile,
configContext = {}, configContext = {},
configOverrides = {}, configOverrides = {},
createRequire = module => isJest ? _createRequire(module.filename) : jiti(module.filename) createRequire = (module: NodeJS.Module) => isJest ? _createRequire(module.filename) : jiti(module.filename)
} = {}) { }: LoadOptions = {}) {
rootDir = path.resolve(rootDir) rootDir = path.resolve(rootDir)
let options = {} let options = {}
@ -120,7 +129,7 @@ export async function loadNuxtConfig ({
return options return options
} }
function loadEnv (envConfig, rootDir = process.cwd()) { function loadEnv (envConfig: EnvConfig, rootDir = process.cwd()) {
const env = Object.create(null) const env = Object.create(null)
// Read dotenv // Read dotenv
@ -147,13 +156,13 @@ function loadEnv (envConfig, rootDir = process.cwd()) {
} }
// Based on https://github.com/motdotla/dotenv-expand // Based on https://github.com/motdotla/dotenv-expand
function expand (target, source = {}, parse = v => v) { function expand (target: Record<string, string>, source: Record<string, string> = {}, parse = (v: string) => v) {
function getValue (key) { function getValue (key: string) {
// Source value 'wins' over target value // Source value 'wins' over target value
return source[key] !== undefined ? source[key] : target[key] return source[key] !== undefined ? source[key] : target[key]
} }
function interpolate (value) { function interpolate (value: string): string {
if (typeof value !== 'string') { if (typeof value !== 'string') {
return value return value
} }
@ -162,7 +171,8 @@ function expand (target, source = {}, parse = v => v) {
const parts = /(.?)\${?([a-zA-Z0-9_:]+)?}?/g.exec(match) const parts = /(.?)\${?([a-zA-Z0-9_:]+)?}?/g.exec(match)
const prefix = parts[1] const prefix = parts[1]
let value, replacePart let value: string
let replacePart: string
if (prefix === '\\') { if (prefix === '\\') {
replacePart = parts[0] replacePart = parts[0]

View File

@ -1,3 +1,4 @@
import { EnvConfig } from 'nuxt/config/load'
import { loadNuxtConfig } from '../config' import { loadNuxtConfig } from '../config'
import Nuxt from './nuxt' import Nuxt from './nuxt'
@ -8,7 +9,19 @@ const OVERRIDES = {
start: { dev: false, _start: true } start: { dev: false, _start: true }
} }
export async function loadNuxt (loadOptions) { export interface LoadOptions {
for?: keyof typeof OVERRIDES
ready?: boolean
rootDir?: string
envConfig?: EnvConfig
configFile?: string
configContext?: {}
configOverrides?: {},
createRequire?: (module: NodeJS.Module) => NodeJS.Require
}
export async function loadNuxt (loadOptions: LoadOptions | LoadOptions['for']) {
// Normalize loadOptions // Normalize loadOptions
if (typeof loadOptions === 'string') { if (typeof loadOptions === 'string') {
loadOptions = { for: loadOptions } loadOptions = { for: loadOptions }

View File

@ -5,8 +5,24 @@ import consola from 'consola'
import { chainFn, sequence } from 'nuxt/utils' import { chainFn, sequence } from 'nuxt/utils'
import Nuxt from './nuxt'
interface Module {
src: string
options: Record<string, any>
handler: () => any
}
interface Template {
}
export default class ModuleContainer { export default class ModuleContainer {
constructor (nuxt) { nuxt: Nuxt
options: Nuxt['options']
requiredModules: Record<string, Module>
constructor (nuxt: Nuxt) {
this.nuxt = nuxt this.nuxt = nuxt
this.options = nuxt.options this.options = nuxt.options
this.requiredModules = {} this.requiredModules = {}
@ -83,7 +99,7 @@ export default class ModuleContainer {
}) })
} }
addLayout (template, name) { addLayout (template, 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]
@ -101,7 +117,7 @@ export default class ModuleContainer {
} }
} }
addErrorLayout (dst) { 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}`
} }
@ -121,13 +137,13 @@ export default class ModuleContainer {
) )
} }
requireModule (moduleOpts) { requireModule (moduleOpts: Module) {
return this.addModule(moduleOpts) return this.addModule(moduleOpts)
} }
async addModule (moduleOpts) { async addModule (moduleOpts) {
let src let src
let options let options: Record<string, any>
let handler let handler
// Type 1: String or Function // Type 1: String or Function
@ -142,7 +158,7 @@ export default class ModuleContainer {
} }
// Define handler if src is a function // Define handler if src is a function
if (typeof src === 'function') { if (src instanceof Function) {
handler = src handler = src
} }

View File

@ -1,7 +1,7 @@
import isPlainObject from 'lodash/isPlainObject' import isPlainObject from 'lodash/isPlainObject'
import consola from 'consola' import consola from 'consola'
import Hookable from 'hable' import Hookable from 'hookable'
import { defineAlias } from 'nuxt/utils' import { defineAlias } from 'nuxt/utils'
import { getNuxtConfig } from 'nuxt/config' import { getNuxtConfig } from 'nuxt/config'
@ -13,6 +13,17 @@ import ModuleContainer from './module'
import Resolver from './resolver' import Resolver from './resolver'
export default class Nuxt extends Hookable { export default class Nuxt extends Hookable {
_ready?: Promise<this>
_initCalled?: boolean
options: any
resolver: Resolver
moduleContainer: ModuleContainer
server?: Server
renderer?: Server
render?: Server['app']
showReady?: () => void
constructor (options = {}) { constructor (options = {}) {
super(consola) super(consola)
@ -103,13 +114,14 @@ export default class Nuxt extends Hookable {
defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen']) defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen'])
} }
async close (callback) { 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()
} }
this.clearHooks() // Deleting as no longer exists on `hookable`
// this.clearHooks()
} }
} }

View File

@ -1,7 +1,7 @@
import { resolve, join } from 'path' import { resolve, join } from 'path'
import fs from 'fs-extra' import fs from 'fs-extra'
import consola from 'consola'
import { Nuxt } from 'nuxt/core'
import { import {
startsWithRootAlias, startsWithRootAlias,
startsWithSrcAlias, startsWithSrcAlias,
@ -9,8 +9,25 @@ import {
clearRequireCache clearRequireCache
} from 'nuxt/utils' } from 'nuxt/utils'
interface ResolvePathOptions {
isAlias?: boolean
isModule?: boolean
isStyle?: boolean
}
interface RequireModuleOptions {
useESM?: boolean
isAlias?: boolean
interopDefault?: any
}
export default class Resolver { export default class Resolver {
constructor (nuxt) { _require: NodeJS.Require
_resolve: RequireResolve
nuxt: Nuxt
options: Nuxt['options']
constructor (nuxt: Nuxt) {
this.nuxt = nuxt this.nuxt = nuxt
this.options = this.nuxt.options this.options = this.nuxt.options
@ -26,7 +43,7 @@ export default class Resolver {
this._resolve = require.resolve this._resolve = require.resolve
} }
resolveModule (path) { resolveModule (path: string) {
try { try {
return this._resolve(path, { return this._resolve(path, {
paths: this.options.modulesDir paths: this.options.modulesDir
@ -42,7 +59,7 @@ export default class Resolver {
} }
} }
resolveAlias (path) { resolveAlias (path: string) {
if (startsWithRootAlias(path)) { if (startsWithRootAlias(path)) {
return join(this.options.rootDir, path.substr(2)) return join(this.options.rootDir, path.substr(2))
} }
@ -54,21 +71,13 @@ export default class Resolver {
return resolve(this.options.srcDir, path) return resolve(this.options.srcDir, path)
} }
resolvePath (path, { alias, isAlias = alias, module, isModule = module, isStyle } = {}) { resolvePath (path: string, { isAlias, isModule, isStyle }: ResolvePathOptions = {}) {
// TODO: Remove in Nuxt 3
if (alias) {
consola.warn('Using alias is deprecated and will be removed in Nuxt 3. Use `isAlias` instead.')
}
if (module) {
consola.warn('Using module is deprecated and will be removed in Nuxt 3. Use `isModule` instead.')
}
// Fast return in case of path exists // Fast return in case of path exists
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
return path return path
} }
let resolvedPath let resolvedPath: string
// Try to resolve it as a regular module // Try to resolve it as a regular module
if (isModule !== false) { if (isModule !== false) {
@ -85,7 +94,7 @@ export default class Resolver {
resolvedPath = path resolvedPath = path
} }
let isDirectory let isDirectory: boolean
// Check if resolvedPath exits and is not a directory // Check if resolvedPath exits and is not a directory
if (fs.existsSync(resolvedPath)) { if (fs.existsSync(resolvedPath)) {
@ -119,22 +128,11 @@ export default class Resolver {
throw new Error(`Cannot resolve "${path}" from "${resolvedPath}"`) throw new Error(`Cannot resolve "${path}" from "${resolvedPath}"`)
} }
requireModule (path, { esm, useESM = esm, alias, isAlias = alias, intropDefault, interopDefault = intropDefault } = {}) { requireModule <T>(path: string, { useESM, isAlias, interopDefault }: RequireModuleOptions = {}): T {
let resolvedPath = path let resolvedPath = path
let requiredModule let requiredModule: any
// TODO: Remove in Nuxt 3 let lastError: any
if (intropDefault) {
consola.warn('Using intropDefault is deprecated and will be removed in Nuxt 3. Use `interopDefault` instead.')
}
if (alias) {
consola.warn('Using alias is deprecated and will be removed in Nuxt 3. Use `isAlias` instead.')
}
if (esm) {
consola.warn('Using esm is deprecated and will be removed in Nuxt 3. Use `useESM` instead.')
}
let lastError
// Try to resolve path // Try to resolve path
try { try {

View File

@ -6,10 +6,32 @@ import defu from 'defu'
import htmlMinifier from 'html-minifier' import htmlMinifier from 'html-minifier'
import { parse } from 'node-html-parser' import { parse } from 'node-html-parser'
import type { Builder } from 'nuxt/builder'
import type { Nuxt } from 'nuxt/core'
import { isFullStatic, flatRoutes, isString, isUrl, promisifyRoute, waitFor, TARGETS } from 'nuxt/utils' import { isFullStatic, flatRoutes, isString, isUrl, promisifyRoute, waitFor, TARGETS } from 'nuxt/utils'
export default class Generator { export default class Generator {
constructor (nuxt, builder) { _payload: null
setPayload: (payload: any) => void
builder?: Builder
isFullStatic: boolean
nuxt: Nuxt
options: Nuxt['options']
staticRoutes: string
srcBuiltPath: string
distPath: string
distNuxtPath: string
staticAssetsDir?: string
staticAssetsBase?: string
payloadDir?: string
routes: Array<{ route: string } & Record<string, any>>
generatedRoutes: Set<string>
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

View File

@ -1,6 +1,8 @@
import type { Nuxt } from 'nuxt/core'
import Generator from './generator' import Generator from './generator'
export { default as Generator } from './generator' export { default as Generator } from './generator'
export function getGenerator (nuxt) { export function getGenerator (nuxt: Nuxt) {
return new Generator(nuxt) return new Generator(nuxt)
} }

View File

@ -1,5 +1,12 @@
import { Server } from 'nuxt/server'
export default class ServerContext { export default class ServerContext {
constructor (server) { nuxt: Server['nuxt']
globals: Server['globals']
options: Server['options']
resources: Server['resources']
constructor (server: Server) {
this.nuxt = server.nuxt this.nuxt = server.nuxt
this.globals = server.globals this.globals = server.globals
this.options = server.options this.options = server.options

View File

@ -1,5 +1,11 @@
import consola from 'consola' import consola from 'consola'
import { timeout } from 'nuxt/utils' import { DeterminedGlobals, timeout } from 'nuxt/utils'
interface Options {
globals: DeterminedGlobals
loadedCallback: string
loadingTimeout?: number
}
export default async function renderAndGetWindow ( export default async function renderAndGetWindow (
url = 'http://localhost:3000', url = 'http://localhost:3000',
@ -8,7 +14,7 @@ export default async function renderAndGetWindow (
loadedCallback, loadedCallback,
loadingTimeout = 2000, loadingTimeout = 2000,
globals globals
} = {} }: Options
) { ) {
const jsdom = await import('jsdom') const jsdom = await import('jsdom')
.then(m => m.default || m) .then(m => m.default || m)
@ -27,13 +33,13 @@ export default async function renderAndGetWindow (
resources: 'usable', resources: 'usable',
runScripts: 'dangerously', runScripts: 'dangerously',
virtualConsole: true, virtualConsole: true,
beforeParse (window) { beforeParse (window: Window) {
// Mock window.scrollTo // Mock window.scrollTo
window.scrollTo = () => {} window.scrollTo = () => {}
} }
}, jsdomOpts) }, jsdomOpts)
const jsdomErrHandler = (err) => { const jsdomErrHandler = (err: any) => {
throw err throw err
} }

View File

@ -1,5 +1,6 @@
import http from 'http' import http from 'http'
import https from 'https' import https from 'https'
import type { ListenOptions } from 'net'
import enableDestroy from 'server-destroy' import enableDestroy from 'server-destroy'
import ip from 'ip' import ip from 'ip'
import consola from 'consola' import consola from 'consola'
@ -8,6 +9,19 @@ import pify from 'pify'
let RANDOM_PORT = '0' let RANDOM_PORT = '0'
export default class Listener { export default class Listener {
port: number | string
host: string
socket: string
https: boolean
app: any
dev: boolean
baseURL: string
listening: boolean
_server: null | http.Server
server: null | http.Server
address: null
url: null | string
constructor ({ port, host, socket, https, app, dev, baseURL }) { constructor ({ port, host, socket, https, app, dev, baseURL }) {
// Options // Options
this.port = port this.port = port
@ -43,6 +57,9 @@ export default class Listener {
computeURL () { computeURL () {
const address = this.server.address() const address = this.server.address()
if (typeof address === 'string') {
return address
}
if (!this.socket) { if (!this.socket) {
switch (address.address) { switch (address.address) {
case '127.0.0.1': this.host = 'localhost'; break case '127.0.0.1': this.host = 'localhost'; break
@ -68,7 +85,7 @@ export default class Listener {
// Call server.listen // Call server.listen
// Prepare listenArgs // Prepare listenArgs
const listenArgs = this.socket ? { path: this.socket } : { host: this.host, port: this.port } const listenArgs: ListenOptions = this.socket ? { path: this.socket } : { host: this.host, port: Number(this.port) }
listenArgs.exclusive = false listenArgs.exclusive = false
// Call server.listen // Call server.listen

View File

@ -1,8 +1,11 @@
import type { ServerResponse } from 'http'
import type { IncomingMessage } from 'connect'
import consola from 'consola' import consola from 'consola'
import onHeaders from 'on-headers' import onHeaders from 'on-headers'
import { Timer } from 'nuxt/utils' import { Timer } from 'nuxt/utils'
export default options => (req, res, next) => { export default options => (_req: IncomingMessage, res: ServerResponse & { timing?: ServerTiming }, next: (err?: any) => void) => {
if (res.timing) { if (res.timing) {
consola.warn('server-timing is already registered.') consola.warn('server-timing is already registered.')
} }
@ -31,13 +34,15 @@ export default options => (req, res, next) => {
} }
class ServerTiming extends Timer { class ServerTiming extends Timer {
constructor (...args) { headers: string[]
super(...args)
constructor () {
super()
this.headers = [] this.headers = []
} }
end (...args) { end (name?: string) {
const time = super.end(...args) const time = super.end(name)
if (time) { if (time) {
this.headers.push(this.formatHeader(time)) this.headers.push(this.formatHeader(time))
} }
@ -49,7 +54,7 @@ class ServerTiming extends Timer {
this.headers.length = 0 this.headers.length = 0
} }
formatHeader (time) { formatHeader (time: ReturnType<Timer['end']>) {
const desc = time.description ? `;desc="${time.description}"` : '' const desc = time.description ? `;desc="${time.description}"` : ''
return `${time.name};dur=${time.duration}${desc}` return `${time.name};dur=${time.duration}${desc}`
} }

View File

@ -1,10 +1,14 @@
import path from 'path' import path from 'path'
import { ServerResponse } from 'http'
import consola from 'consola' import consola from 'consola'
import launchMiddleware from 'launch-editor-middleware' import launchMiddleware from 'launch-editor-middleware'
import serveStatic from 'serve-static' import serveStatic from 'serve-static'
import servePlaceholder from 'serve-placeholder' import servePlaceholder from 'serve-placeholder'
import connect from 'connect' import connect, { IncomingMessage } from 'connect'
import { determineGlobals, isUrl } from 'nuxt/utils' import type { TemplateExecutor } from 'lodash'
import { Nuxt } from 'nuxt/core'
import { DeterminedGlobals, determineGlobals, isUrl } from 'nuxt/utils'
import { VueRenderer } from 'nuxt/vue-renderer' import { VueRenderer } from 'nuxt/vue-renderer'
import ServerContext from './context' import ServerContext from './context'
@ -14,8 +18,34 @@ import errorMiddleware from './middleware/error'
import Listener from './listener' import Listener from './listener'
import createTimingMiddleware from './middleware/timing' import createTimingMiddleware from './middleware/timing'
interface Manifest {
assetsMapping: Record<string, string[]>
publicPath: string
}
export default class Server { export default class Server {
constructor (nuxt) { __closed?: boolean
_readyCalled?: boolean
app: connect.Server
devMiddleware: (req: IncomingMessage, res: ServerResponse, next: (err?: any) => void) => any
listeners: Listener[]
nuxt: Nuxt
globals: DeterminedGlobals
options: Nuxt['options']
publicPath: boolean
renderer: VueRenderer
resources: {
clientManifest?: Manifest
modernManifest?: Manifest
serverManifest?: Manifest
ssrTemplate?: TemplateExecutor
spaTemplate?: TemplateExecutor
errorTemplate?: TemplateExecutor
}
serverContext: ServerContext
constructor (nuxt: Nuxt) {
this.nuxt = nuxt this.nuxt = nuxt
this.options = nuxt.options this.options = nuxt.options
@ -76,7 +106,7 @@ export default class Server {
const { compressor } = this.options.render const { compressor } = this.options.render
if (typeof compressor === 'object') { if (typeof compressor === 'object') {
// If only setting for `compression` are provided, require the module and insert // If only setting for `compression` are provided, require the module and insert
const compression = this.nuxt.resolver.requireModule('compression') const compression = this.nuxt.resolver.requireModule<typeof import('compression')>('compression')
this.useMiddleware(compression(compressor)) this.useMiddleware(compression(compressor))
} else if (compressor) { } else if (compressor) {
// Else, require own compression middleware if compressor is actually truthy // Else, require own compression middleware if compressor is actually truthy
@ -317,12 +347,12 @@ export default class Server {
return this.app.stack.map(({ handle }) => handle._middleware && handle._middleware.entry).filter(Boolean) return this.app.stack.map(({ handle }) => handle._middleware && handle._middleware.entry).filter(Boolean)
} }
renderRoute () { renderRoute (...args: Parameters<VueRenderer['renderRoute']>) {
return this.renderer.renderRoute.apply(this.renderer, arguments) return this.renderer.renderRoute.apply(this.renderer, ...args.slice())
} }
loadResources () { loadResources (...args: Parameters<VueRenderer['loadResources']>) {
return this.renderer.loadResources.apply(this.renderer, arguments) return this.renderer.loadResources.apply(this.renderer, ...args)
} }
renderAndGetWindow (url, opts = {}, { renderAndGetWindow (url, opts = {}, {
@ -337,13 +367,13 @@ export default class Server {
}) })
} }
async listen (port, host, socket) { async listen (port?: string | number, host?: string, socket?: string) {
// Ensure nuxt is ready // Ensure nuxt is ready
await this.nuxt.ready() await this.nuxt.ready()
// Create a new listener // Create a new listener
const listener = new Listener({ const listener = new Listener({
port: isNaN(parseInt(port)) ? this.options.server.port : port, port: typeof port !== 'number' && isNaN(parseInt(port)) ? this.options.server.port : port,
host: host || this.options.server.host, host: host || this.options.server.host,
socket: socket || this.options.server.socket, socket: socket || this.options.server.socket,
https: this.options.server.https, https: this.options.server.https,

View File

@ -1,10 +1,10 @@
import { join } from 'path' import { join } from 'path'
export function isExternalDependency (id) { export function isExternalDependency (id: string) {
return /[/\\]node_modules[/\\]/.test(id) return /[/\\]node_modules[/\\]/.test(id)
} }
export function clearRequireCache (id) { export function clearRequireCache (id: string) {
if (isExternalDependency(id)) { if (isExternalDependency(id)) {
return return
} }
@ -27,7 +27,7 @@ export function clearRequireCache (id) {
delete require.cache[id] delete require.cache[id]
} }
export function scanRequireTree (id, files = new Set()) { export function scanRequireTree (id: string, files = new Set<string>()) {
if (isExternalDependency(id) || files.has(id)) { if (isExternalDependency(id) || files.has(id)) {
return files return files
} }
@ -48,20 +48,20 @@ export function scanRequireTree (id, files = new Set()) {
return files return files
} }
export function getRequireCacheItem (id) { export function getRequireCacheItem (id: string) {
try { try {
return require.cache[id] return require.cache[id]
} catch (e) { } catch (e) {
} }
} }
export function tryRequire (id) { export function tryRequire (id: string) {
try { try {
return require(id) return require(id)
} catch (e) { } catch (e) {
} }
} }
export function getPKG (id) { export function getPKG (id: string) {
return tryRequire(join(id, 'package.json')) return tryRequire(join(id, 'package.json'))
} }

View File

@ -1,9 +1,13 @@
export const TARGETS = { export const TARGETS = {
server: 'server', server: 'server',
static: 'static' static: 'static'
} } as const
export type Target = keyof typeof TARGETS
export const MODES = { export const MODES = {
universal: 'universal', universal: 'universal',
spa: 'spa' spa: 'spa'
} } as const
export type Mode = keyof typeof MODES

View File

@ -1,19 +1,35 @@
import type { ServerResponse } from 'http'
import type { IncomingMessage } from 'connect'
import { TARGETS } from './constants' import { TARGETS } from './constants'
export const getContext = function getContext (req, res) { export const getContext = function getContext (req: IncomingMessage, res: ServerResponse) {
return { req, res } return { req, res }
} }
export const determineGlobals = function determineGlobals (globalName, globals) { type NuxtGlobal = string | ((globalName: string) => string)
const _globals = {}
type Globals = 'id' | 'nuxt' | 'context' | 'pluginPrefix' | 'readyCallback' | 'loadedCallback'
type NuxtGlobals = {
[key in Globals]: NuxtGlobal
}
export type DeterminedGlobals = {
[key in keyof NuxtGlobals]: string
}
export const determineGlobals = function determineGlobals (globalName: string, globals: NuxtGlobals) {
const _globals: Partial<DeterminedGlobals> = {}
for (const global in globals) { for (const global in globals) {
if (typeof globals[global] === 'function') { const currentGlobal = globals[global]
_globals[global] = globals[global](globalName) if (currentGlobal instanceof Function) {
_globals[global] = currentGlobal(globalName)
} else { } else {
_globals[global] = globals[global] _globals[global] = currentGlobal
} }
} }
return _globals return _globals as DeterminedGlobals
} }
export const isFullStatic = function (options) { export const isFullStatic = function (options) {

View File

@ -1,20 +1,25 @@
export const encodeHtml = function encodeHtml (str) { export const encodeHtml = function encodeHtml (str: string) {
return str.replace(/</g, '&lt;').replace(/>/g, '&gt;') return str.replace(/</g, '&lt;').replace(/>/g, '&gt;')
} }
export const isString = obj => typeof obj === 'string' || obj instanceof String export const isString = (obj: unknown): obj is string =>
typeof obj === 'string' || obj instanceof String
export const isNonEmptyString = obj => Boolean(obj && isString(obj)) export const isNonEmptyString = (obj: unknown): obj is string =>
Boolean(obj && isString(obj))
export const isPureObject = obj => !Array.isArray(obj) && typeof obj === 'object' export const isPureObject = (
obj: unknown
): obj is Exclude<object, Array<any>> =>
!Array.isArray(obj) && typeof obj === 'object'
export const isUrl = function isUrl (url) { export const isUrl = function isUrl (url: string) {
return ['http', '//'].some(str => url.startsWith(str)) return ['http', '//'].some(str => url.startsWith(str))
} }
export const urlJoin = function urlJoin () { export const urlJoin = function urlJoin (...args: string[]) {
return [].slice return [].slice
.call(arguments) .call(args)
.join('/') .join('/')
.replace(/\/+/g, '/') .replace(/\/+/g, '/')
.replace(':/', '://') .replace(':/', '://')
@ -22,13 +27,11 @@ export const urlJoin = function urlJoin () {
/** /**
* Wraps value in array if it is not already an array * Wraps value in array if it is not already an array
*
* @param {any} value
* @return {array}
*/ */
export const wrapArray = value => Array.isArray(value) ? value : [value] export const wrapArray = <T>(value: T | T[]): T[] =>
Array.isArray(value) ? value : [value]
const WHITESPACE_REPLACEMENTS = [ const WHITESPACE_REPLACEMENTS: [RegExp, string][] = [
[/[ \t\f\r]+\n/g, '\n'], // strip empty indents [/[ \t\f\r]+\n/g, '\n'], // strip empty indents
[/{\n{2,}/g, '{\n'], // strip start padding from blocks [/{\n{2,}/g, '{\n'], // strip start padding from blocks
[/\n{2,}([ \t\f\r]*})/g, '\n$1'], // strip end padding from blocks [/\n{2,}([ \t\f\r]*})/g, '\n$1'], // strip end padding from blocks
@ -36,7 +39,7 @@ const WHITESPACE_REPLACEMENTS = [
[/\n{2,}$/g, '\n'] // strip blank lines EOF (0 allowed) [/\n{2,}$/g, '\n'] // strip blank lines EOF (0 allowed)
] ]
export const stripWhitespace = function stripWhitespace (string) { export const stripWhitespace = function stripWhitespace (string: string) {
WHITESPACE_REPLACEMENTS.forEach(([regex, newSubstr]) => { WHITESPACE_REPLACEMENTS.forEach(([regex, newSubstr]) => {
string = string.replace(regex, newSubstr) string = string.replace(regex, newSubstr)
}) })

View File

@ -2,27 +2,42 @@ import path from 'path'
import consola from 'consola' import consola from 'consola'
import hash from 'hash-sum' import hash from 'hash-sum'
import fs from 'fs-extra' import fs from 'fs-extra'
import properlock from 'proper-lockfile' import properlock, { LockOptions } from 'proper-lockfile'
import onExit from 'signal-exit' import onExit from 'signal-exit'
export const lockPaths = new Set() export const lockPaths = new Set<string>()
export const defaultLockOptions = { export const defaultLockOptions: Required<
Pick<LockOptions, 'stale' | 'onCompromised'>
> = {
stale: 30000, stale: 30000,
onCompromised: err => consola.warn(err) onCompromised: err => consola.warn(err)
} }
export function getLockOptions (options) { export function getLockOptions (options: Partial<LockOptions>) {
return Object.assign({}, defaultLockOptions, options) return Object.assign({}, defaultLockOptions, options)
} }
export function createLockPath ({ id = 'nuxt', dir, root }) { interface NuxtLockOptions {
id?: string
dir: string
root: string
options?: LockOptions
}
export function createLockPath ({
id = 'nuxt',
dir,
root
}: Pick<NuxtLockOptions, 'id' | 'dir' | 'root'>) {
const sum = hash(`${root}-${dir}`) const sum = hash(`${root}-${dir}`)
return path.resolve(root, 'node_modules/.cache/nuxt', `${id}-lock-${sum}`) return path.resolve(root, 'node_modules/.cache/nuxt', `${id}-lock-${sum}`)
} }
export async function getLockPath (config) { export async function getLockPath (
config: Pick<NuxtLockOptions, 'id' | 'dir' | 'root'>
) {
const lockPath = createLockPath(config) const lockPath = createLockPath(config)
// the lock is created for the lockPath as ${lockPath}.lock // the lock is created for the lockPath as ${lockPath}.lock
@ -32,8 +47,12 @@ export async function getLockPath (config) {
return lockPath return lockPath
} }
export async function lock ({ id, dir, root, options }) { export async function lock ({ id, dir, root, options }: NuxtLockOptions) {
const lockPath = await getLockPath({ id, dir, root }) const lockPath = await getLockPath({
id,
dir,
root
})
try { try {
const locked = await properlock.check(lockPath) const locked = await properlock.check(lockPath)
@ -45,7 +64,7 @@ export async function lock ({ id, dir, root, options }) {
} }
let lockWasCompromised = false let lockWasCompromised = false
let release let release: (() => Promise<void>) | undefined
try { try {
options = getLockOptions(options) options = getLockOptions(options)
@ -94,7 +113,7 @@ export async function lock ({ id, dir, root, options }) {
// as well, but in our case its much more likely the lock was // as well, but in our case its much more likely the lock was
// compromised due to mtime update timeouts // compromised due to mtime update timeouts
const lockDir = `${lockPath}.lock` const lockDir = `${lockPath}.lock`
if (await fs.exists(lockDir)) { if (await fs.pathExists(lockDir)) {
await fs.remove(lockDir) await fs.remove(lockDir)
} }
} }

View File

@ -1,4 +1,7 @@
import UAParser from 'ua-parser-js' import { UAParser } from 'ua-parser-js'
import type { SemVer } from 'semver'
import type { IncomingMessage } from 'connect'
export const ModernBrowsers = { export const ModernBrowsers = {
Edge: '16', Edge: '16',
@ -12,49 +15,65 @@ export const ModernBrowsers = {
Yandex: '18', Yandex: '18',
Vivaldi: '1.14', Vivaldi: '1.14',
'Mobile Safari': '10.3' 'Mobile Safari': '10.3'
} } as const
let semver type ModernBrowsers = { -readonly [key in keyof typeof ModernBrowsers]: SemVer }
let __modernBrowsers
let semver: typeof import('semver')
let __modernBrowsers: ModernBrowsers
const getModernBrowsers = () => { const getModernBrowsers = () => {
if (__modernBrowsers) { if (__modernBrowsers) {
return __modernBrowsers return __modernBrowsers
} }
__modernBrowsers = Object.keys(ModernBrowsers) __modernBrowsers = (Object.keys(ModernBrowsers) as Array<
.reduce((allBrowsers, browser) => { keyof typeof ModernBrowsers
allBrowsers[browser] = semver.coerce(ModernBrowsers[browser]) >).reduce(
(allBrowsers, browser) => {
const version = semver.coerce(ModernBrowsers[browser])
if (version) { allBrowsers[browser] = version }
return allBrowsers return allBrowsers
}, {}) },
{} as ModernBrowsers
)
return __modernBrowsers return __modernBrowsers
} }
export const isModernBrowser = (ua) => { interface NuxtRequest extends IncomingMessage {
socket: IncomingMessage['socket'] & {
_modern?: boolean
}
}
export const isModernBrowser = (ua: string) => {
if (!ua) { if (!ua) {
return false return false
} }
if (!semver) { if (!semver) {
semver = require('semver') semver = require('semver')
} }
const { browser } = UAParser(ua) const browser = new UAParser(ua).getBrowser()
const browserVersion = semver.coerce(browser.version) const browserVersion = semver.coerce(browser.version)
if (!browserVersion) { if (!browserVersion) {
return false return false
} }
const modernBrowsers = getModernBrowsers() const modernBrowsers = getModernBrowsers()
return Boolean(modernBrowsers[browser.name] && semver.gte(browserVersion, modernBrowsers[browser.name])) const name = browser.name as keyof typeof modernBrowsers
return Boolean(
name && name in modernBrowsers && semver.gte(browserVersion, modernBrowsers[name])
)
} }
export const isModernRequest = (req, modernMode = false) => { export const isModernRequest = (req: NuxtRequest, modernMode = false) => {
if (modernMode === false) { if (modernMode === false) {
return false return false
} }
const { socket = {}, headers } = req const { socket = {} as NuxtRequest['socket'], headers } = req
if (socket._modern === undefined) { if (socket._modern === undefined) {
const ua = headers && headers['user-agent'] const ua = headers && headers['user-agent']
socket._modern = isModernBrowser(ua) socket._modern = ua && isModernBrowser(ua)
} }
return socket._modern return socket._modern

View File

@ -2,7 +2,8 @@ import path from 'path'
import consola from 'consola' import consola from 'consola'
import escapeRegExp from 'lodash/escapeRegExp' import escapeRegExp from 'lodash/escapeRegExp'
export const startsWithAlias = aliasArray => str => aliasArray.some(c => str.startsWith(c)) export const startsWithAlias = (aliasArray: string[]) => (str: string) =>
aliasArray.some(c => str.startsWith(c))
export const startsWithSrcAlias = startsWithAlias(['@', '~']) export const startsWithSrcAlias = startsWithAlias(['@', '~'])
@ -24,9 +25,9 @@ export const wChunk = function wChunk (p = '') {
const reqSep = /\//g const reqSep = /\//g
const sysSep = escapeRegExp(path.sep) const sysSep = escapeRegExp(path.sep)
const normalize = string => string.replace(reqSep, sysSep) const normalize = (string: string) => string.replace(reqSep, sysSep)
export const r = function r (...args) { export const r = function r (...args: string[]) {
const lastArg = args[args.length - 1] const lastArg = args[args.length - 1]
if (startsWithSrcAlias(lastArg)) { if (startsWithSrcAlias(lastArg)) {
@ -36,14 +37,12 @@ export const r = function r (...args) {
return wp(path.resolve(...args.map(normalize))) return wp(path.resolve(...args.map(normalize)))
} }
export const relativeTo = function relativeTo (...args) { export const relativeTo = function relativeTo (dir: string, ...args: string[]): string {
const dir = args.shift()
// Keep webpack inline loader intact // Keep webpack inline loader intact
if (args[0].includes('!')) { if (args[0].includes('!')) {
const loaders = args.shift().split('!') const loaders = args.shift()!.split('!')
return loaders.concat(relativeTo(dir, loaders.pop(), ...args)).join('!') return loaders.concat(relativeTo(dir, loaders.pop()!, ...args)).join('!')
} }
// Resolve path // Resolve path
@ -63,7 +62,17 @@ export const relativeTo = function relativeTo (...args) {
return wp(rp) return wp(rp)
} }
export function defineAlias (src, target, prop, opts = {}) { interface AliasOptions {
bind?: boolean
warn?: boolean
}
export function defineAlias (
src: string,
target: Record<string, any>,
prop: string | string[],
opts: AliasOptions = {}
) {
const { bind = true, warn = false } = opts const { bind = true, warn = false } = opts
if (Array.isArray(prop)) { if (Array.isArray(prop)) {
@ -94,9 +103,9 @@ export function defineAlias (src, target, prop, opts = {}) {
}) })
} }
const isIndex = s => /(.*)\/index\.[^/]+$/.test(s) const isIndex = (s: string) => /(.*)\/index\.[^/]+$/.test(s)
export function isIndexFileAndFolder (pluginFiles) { export function isIndexFileAndFolder (pluginFiles: string[]) {
// Return early in case the matching file count exceeds 2 (index.js + folder) // Return early in case the matching file count exceeds 2 (index.js + folder)
if (pluginFiles.length !== 2) { if (pluginFiles.length !== 2) {
return false return false
@ -105,5 +114,9 @@ export function isIndexFileAndFolder (pluginFiles) {
} }
export const getMainModule = () => { export const getMainModule = () => {
return require.main || (module && module.main) || module return (
require.main ||
(module && ((module as any).main as NodeJS.Module)) ||
module
)
} }

View File

@ -2,9 +2,12 @@ import path from 'path'
import get from 'lodash/get' import get from 'lodash/get'
import consola from 'consola' import consola from 'consola'
import type { Component } from 'vue'
import type { _RouteRecordBase } from 'vue-router'
import { r } from './resolve' import { r } from './resolve'
export const flatRoutes = function flatRoutes (router, fileName = '', routes = []) { export const flatRoutes = function flatRoutes (router, fileName = '', routes: string[] = []) {
router.forEach((r) => { router.forEach((r) => {
if ([':', '*'].some(c => r.path.includes(c))) { if ([':', '*'].some(c => r.path.includes(c))) {
return return
@ -30,12 +33,12 @@ export const flatRoutes = function flatRoutes (router, fileName = '', routes = [
return routes return routes
} }
function cleanChildrenRoutes (routes, isChild = false, routeNameSplitter = '-') { function cleanChildrenRoutes (routes: NuxtRouteConfig[], isChild = false, routeNameSplitter = '-') {
let start = -1 let start = -1
const regExpIndex = new RegExp(`${routeNameSplitter}index$`) const regExpIndex = new RegExp(`${routeNameSplitter}index$`)
const routesIndex = [] const routesIndex: string[][] = []
routes.forEach((route) => { routes.forEach((route) => {
if (regExpIndex.test(route.name) || route.name === 'index') { if (route.name && typeof route.name === 'string' && (regExpIndex.test(route.name) || route.name === 'index')) {
// Save indexOf 'index' key in name // Save indexOf 'index' key in name
const res = route.name.split(routeNameSplitter) const res = route.name.split(routeNameSplitter)
const s = res.indexOf('index') const s = res.indexOf('index')
@ -46,7 +49,7 @@ function cleanChildrenRoutes (routes, isChild = false, routeNameSplitter = '-')
routes.forEach((route) => { routes.forEach((route) => {
route.path = isChild ? route.path.replace('/', '') : route.path route.path = isChild ? route.path.replace('/', '') : route.path
if (route.path.includes('?')) { if (route.path.includes('?')) {
const names = route.name.split(routeNameSplitter) const names = typeof route.name === 'string' && route.name.split(routeNameSplitter) || []
const paths = route.path.split('/') const paths = route.path.split('/')
if (!isChild) { if (!isChild) {
paths.shift() paths.shift()
@ -66,7 +69,9 @@ function cleanChildrenRoutes (routes, isChild = false, routeNameSplitter = '-')
}) })
route.path = (isChild ? '' : '/') + paths.join('/') route.path = (isChild ? '' : '/') + paths.join('/')
} }
route.name = route.name.replace(regExpIndex, '') if (route.name) {
route.name = typeof route.name === 'string' && route.name.replace(regExpIndex, '')
}
if (route.children) { if (route.children) {
if (route.children.find(child => child.path === '')) { if (route.children.find(child => child.path === '')) {
delete route.name delete route.name
@ -79,7 +84,7 @@ function cleanChildrenRoutes (routes, isChild = false, routeNameSplitter = '-')
const DYNAMIC_ROUTE_REGEX = /^\/([:*])/ const DYNAMIC_ROUTE_REGEX = /^\/([:*])/
export const sortRoutes = function sortRoutes (routes) { export const sortRoutes = function sortRoutes (routes: NuxtRouteConfig[]) {
routes.sort((a, b) => { routes.sort((a, b) => {
if (!a.path.length) { if (!a.path.length) {
return -1 return -1
@ -136,6 +141,22 @@ export const sortRoutes = function sortRoutes (routes) {
return routes return routes
} }
interface CreateRouteOptions {
files: string[]
srcDir: string
pagesDir?: string
routeNameSplitter?: string
supportedExtensions?: string[]
trailingSlash: boolean
}
interface NuxtRouteConfig extends Omit<_RouteRecordBase, 'children'> {
component?: Component | string
chunkName?: string
pathToRegexpOptions?: any
children?: NuxtRouteConfig[]
}
export const createRoutes = function createRoutes ({ export const createRoutes = function createRoutes ({
files, files,
srcDir, srcDir,
@ -143,8 +164,8 @@ export const createRoutes = function createRoutes ({
routeNameSplitter = '-', routeNameSplitter = '-',
supportedExtensions = ['vue', 'js'], supportedExtensions = ['vue', 'js'],
trailingSlash trailingSlash
}) { }: CreateRouteOptions) {
const routes = [] const routes: NuxtRouteConfig[] = []
files.forEach((file) => { files.forEach((file) => {
const keys = file const keys = file
.replace(new RegExp(`^${pagesDir}`), '') .replace(new RegExp(`^${pagesDir}`), '')
@ -152,13 +173,13 @@ export const createRoutes = function createRoutes ({
.replace(/\/{2,}/g, '/') .replace(/\/{2,}/g, '/')
.split('/') .split('/')
.slice(1) .slice(1)
const route = { name: '', path: '', component: r(srcDir, file) } const route: NuxtRouteConfig = { name: '', path: '', component: r(srcDir, file) }
let parent = routes let parent = routes
keys.forEach((key, i) => { keys.forEach((key, i) => {
// remove underscore only, if its the prefix // remove underscore only, if its the prefix
const sanitizedKey = key.startsWith('_') ? key.substr(1) : key const sanitizedKey = key.startsWith('_') ? key.substr(1) : key
route.name = route.name route.name = route.name && typeof route.name === 'string'
? route.name + routeNameSplitter + sanitizedKey ? route.name + routeNameSplitter + sanitizedKey
: sanitizedKey : sanitizedKey
route.name += key === '_' ? 'all' : '' route.name += key === '_' ? 'all' : ''
@ -192,9 +213,9 @@ 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, key1, key2) { export const guardDir = function guardDir (options: Record<string, unknown>, key1: string, key2: string) {
const dir1 = get(options, key1, false) const dir1 = get(options, key1, false) as string
const dir2 = get(options, key2, false) const dir2 = get(options, key2, false) as string
if ( if (
dir1 && dir1 &&
@ -213,7 +234,7 @@ export const guardDir = function guardDir (options, key1, key2) {
} }
} }
const getRoutePathExtension = (key) => { const getRoutePathExtension = (key: string) => {
if (key === '_') { if (key === '_') {
return '*' return '*'
} }

View File

@ -1,6 +1,10 @@
import serialize from 'serialize-javascript' import serialize from 'serialize-javascript'
export function normalizeFunctions (obj) { export function normalizeFunctions (obj: Array<any>): Array<any>
export function normalizeFunctions (obj: null): null
export function normalizeFunctions (obj: Function): Function
export function normalizeFunctions (obj: Record<string, any>): Record<string, any>
export function normalizeFunctions (obj: Array<unknown> | null | Function | Record<string, any>) {
if (typeof obj !== 'object' || Array.isArray(obj) || obj === null) { if (typeof obj !== 'object' || Array.isArray(obj) || obj === null) {
return obj return obj
} }
@ -22,14 +26,14 @@ export function normalizeFunctions (obj) {
functionBody = `return ${functionBody}` functionBody = `return ${functionBody}`
} }
// eslint-disable-next-line no-new-func // eslint-disable-next-line no-new-func
obj[key] = new Function(...match[1].split(',').map(arg => arg.trim()), functionBody) obj[key] = new Function(...match[1].split(',').map((arg: string) => arg.trim()), functionBody)
} }
} }
} }
return obj return obj
} }
export function serializeFunction (func) { export function serializeFunction (func: Function) {
let open = false let open = false
func = normalizeFunctions(func) func = normalizeFunctions(func)
return serialize(func) return serialize(func)

View File

@ -1,11 +1,17 @@
export const sequence = function sequence (tasks, fn) { export const sequence = function sequence<T, R> (
tasks: T[],
fn: (task: T) => R
) {
return tasks.reduce( return tasks.reduce(
(promise, task) => promise.then(() => fn(task)), (promise, task): any => promise.then(() => fn(task)),
Promise.resolve() Promise.resolve()
) )
} }
export const parallel = function parallel (tasks, fn) { export const parallel = function parallel<T, R> (
tasks: T[],
fn: (task: T) => R
) {
return Promise.all(tasks.map(fn)) return Promise.all(tasks.map(fn))
} }

View File

@ -1,5 +1,8 @@
async function promiseFinally (fn, finalFn) { async function promiseFinally<T> (
let result fn: (() => Promise<T>) | Promise<T>,
finalFn: () => any
) {
let result: T
try { try {
if (typeof fn === 'function') { if (typeof fn === 'function') {
result = await fn() result = await fn()
@ -12,8 +15,12 @@ async function promiseFinally (fn, finalFn) {
return result return result
} }
export const timeout = function timeout (fn, ms, msg) { export const timeout = function timeout (
let timerId fn: () => any,
ms: number,
msg: string
) {
let timerId: NodeJS.Timeout
const warpPromise = promiseFinally(fn, () => clearTimeout(timerId)) const warpPromise = promiseFinally(fn, () => clearTimeout(timerId))
const timerPromise = new Promise((resolve, reject) => { const timerPromise = new Promise((resolve, reject) => {
timerId = setTimeout(() => reject(new Error(msg)), ms) timerId = setTimeout(() => reject(new Error(msg)), ms)
@ -21,16 +28,25 @@ export const timeout = function timeout (fn, ms, msg) {
return Promise.race([warpPromise, timerPromise]) return Promise.race([warpPromise, timerPromise])
} }
export const waitFor = function waitFor (ms) { export const waitFor = function waitFor (ms: number) {
return new Promise(resolve => setTimeout(resolve, ms || 0)) return new Promise(resolve => setTimeout(resolve, ms || 0))
} }
interface Time {
name: string
description: string
start: [number, number] | bigint
duration?: bigint | [number, number]
}
export class Timer { export class Timer {
_times: Map<string, Time>
constructor () { constructor () {
this._times = new Map() this._times = new Map()
} }
start (name, description) { start (name: string, description: string) {
const time = { const time: Time = {
name, name,
description, description,
start: this.hrtime() start: this.hrtime()
@ -39,22 +55,33 @@ export class Timer {
return time return time
} }
end (name) { end (name: string) {
if (this._times.has(name)) { if (this._times.has(name)) {
const time = this._times.get(name) const time = this._times.get(name)!
if (typeof time.start === 'bigint') {
time.duration = this.hrtime(time.start) time.duration = this.hrtime(time.start)
} else {
time.duration = this.hrtime(time.start)
}
this._times.delete(name) this._times.delete(name)
return time return time
} }
} }
hrtime (start) { hrtime (start?: bigint): bigint
hrtime (start?: [number, number]): [number, number]
hrtime (start?: [number, number] | bigint) {
const useBigInt = typeof process.hrtime.bigint === 'function' const useBigInt = typeof process.hrtime.bigint === 'function'
if (start) { if (start) {
const end = useBigInt ? process.hrtime.bigint() : process.hrtime(start) if (typeof start === 'bigint') {
return useBigInt if (!useBigInt) { throw new Error('bigint is not supported.') }
? (end - start) / BigInt(1000000)
: (end[0] * 1e3) + (end[1] * 1e-6) const end = process.hrtime.bigint()
return (end - start) / BigInt(1000000)
}
const end = process.hrtime(start)
return end[0] * 1e3 + end[1] * 1e-6
} }
return useBigInt ? process.hrtime.bigint() : process.hrtime() return useBigInt ? process.hrtime.bigint() : process.hrtime()
} }

View File

@ -7,9 +7,22 @@ import { TARGETS, isModernRequest, waitFor } from 'nuxt/utils'
import SPARenderer from './renderers/spa' import SPARenderer from './renderers/spa'
import SSRRenderer from './renderers/ssr' import SSRRenderer from './renderers/ssr'
import ModernRenderer from './renderers/modern' import ModernRenderer from './renderers/modern'
import ServerContext from 'nuxt/server/context'
export default class VueRenderer { export default class VueRenderer {
constructor (context) { __closed?: boolean
_state?: 'created' | 'loading' | 'ready' | 'error'
_error?: null
_readyPromise?: Promise<any>
distPath: string
serverContext: ServerContext
renderer: {
ssr: any
modern: any
spa: any
}
constructor (context: ServerContext) {
this.serverContext = context this.serverContext = context
this.options = this.serverContext.options this.options = this.serverContext.options
@ -86,10 +99,10 @@ export default class VueRenderer {
} }
} }
async loadResources (_fs) { async loadResources (_fs: typeof import('fs-extra')) {
const updated = [] const updated = []
const readResource = async (fileName, encoding) => { const readResource = async (fileName: string, encoding: string) => {
try { try {
const fullPath = path.resolve(this.distPath, fileName) const fullPath = path.resolve(this.distPath, fileName)
@ -311,16 +324,16 @@ export default class VueRenderer {
return { return {
clientManifest: { clientManifest: {
fileName: 'client.manifest.json', fileName: 'client.manifest.json',
transform: src => JSON.parse(src) transform: (src: string) => JSON.parse(src)
}, },
modernManifest: { modernManifest: {
fileName: 'modern.manifest.json', fileName: 'modern.manifest.json',
transform: src => JSON.parse(src) transform: (src: string) => JSON.parse(src)
}, },
serverManifest: { serverManifest: {
fileName: 'server.manifest.json', fileName: 'server.manifest.json',
// BundleRenderer needs resolved contents // BundleRenderer needs resolved contents
transform: async (src, { readResource }) => { transform: async (src: string, { readResource }) => {
const serverManifest = JSON.parse(src) const serverManifest = JSON.parse(src)
const readResources = async (obj) => { const readResources = async (obj) => {
@ -357,16 +370,16 @@ export default class VueRenderer {
}, },
ssrTemplate: { ssrTemplate: {
fileName: 'index.ssr.html', fileName: 'index.ssr.html',
transform: src => this.parseTemplate(src) transform: (src: string) => this.parseTemplate(src)
}, },
spaTemplate: { spaTemplate: {
fileName: 'index.spa.html', fileName: 'index.spa.html',
transform: src => this.parseTemplate(src) transform: (src: string) => this.parseTemplate(src)
} }
} }
} }
parseTemplate (templateStr) { parseTemplate (templateStr: string) {
return template(templateStr, { return template(templateStr, {
interpolate: /{{([\s\S]+?)}}/g, interpolate: /{{([\s\S]+?)}}/g,
evaluate: /{%([\s\S]+?)%}/g evaluate: /{%([\s\S]+?)%}/g

View File

@ -1,10 +1,15 @@
import ServerContext from "nuxt/server/context"
export default class BaseRenderer { export default class BaseRenderer {
constructor (serverContext) { serverContext: ServerContext
options: ServerContext['options']
constructor (serverContext: ServerContext) {
this.serverContext = serverContext this.serverContext = serverContext
this.options = serverContext.options this.options = serverContext.options
} }
renderTemplate (templateFn, opts) { renderTemplate (templateFn: (options: Record<string, any>) => void, opts: Record<string, any>) {
// Fix problem with HTMLPlugin's minify option (#3392) // Fix problem with HTMLPlugin's minify option (#3392)
opts.html_attrs = opts.HTML_ATTRS opts.html_attrs = opts.HTML_ATTRS
opts.head_attrs = opts.HEAD_ATTRS opts.head_attrs = opts.HEAD_ATTRS
@ -13,7 +18,7 @@ export default class BaseRenderer {
return templateFn(opts) return templateFn(opts)
} }
render () { render (renderContext) {
throw new Error('`render()` needs to be implemented') throw new Error('`render()` needs to be implemented')
} }
} }

View File

@ -1,8 +1,13 @@
import ServerContext from 'nuxt/server/context'
import { isUrl, urlJoin, safariNoModuleFix } from 'nuxt/utils' import { isUrl, urlJoin, safariNoModuleFix } from 'nuxt/utils'
import SSRRenderer from './ssr' import SSRRenderer from './ssr'
export default class ModernRenderer extends SSRRenderer { export default class ModernRenderer extends SSRRenderer {
constructor (serverContext) { _assetsMapping?: Record<string, string>
publicPath: string
constructor (serverContext: ServerContext) {
super(serverContext) super(serverContext)
const { build: { publicPath }, router: { base } } = this.options const { build: { publicPath }, router: { base } } = this.options
@ -17,7 +22,7 @@ export default class ModernRenderer extends SSRRenderer {
const { clientManifest, modernManifest } = this.serverContext.resources const { clientManifest, modernManifest } = this.serverContext.resources
const legacyAssets = clientManifest.assetsMapping const legacyAssets = clientManifest.assetsMapping
const modernAssets = modernManifest.assetsMapping const modernAssets = modernManifest.assetsMapping
const mapping = {} const mapping: Record<string, string> = {}
Object.keys(legacyAssets).forEach((componentHash) => { Object.keys(legacyAssets).forEach((componentHash) => {
const modernComponentAssets = modernAssets[componentHash] || [] const modernComponentAssets = modernAssets[componentHash] || []

View File

@ -3,11 +3,22 @@ import cloneDeep from 'lodash/cloneDeep'
import VueMeta from 'vue-meta' import VueMeta from 'vue-meta'
import LRU from 'lru-cache' import LRU from 'lru-cache'
import devalue from '@nuxt/devalue' import devalue from '@nuxt/devalue'
import { TARGETS, isModernRequest } from 'nuxt/utils' import { TARGETS, isModernRequest } from 'nuxt/utils'
import ServerContext from 'nuxt/server/context'
import BaseRenderer from './base' import BaseRenderer from './base'
export default class SPARenderer extends BaseRenderer { export default class SPARenderer extends BaseRenderer {
constructor (serverContext) { cache: LRU<unknown, unknown>
vueMetaConfig: {
ssrAppId: string
keyName: string
attribute: string
ssrAttribute: string
tagIDKeyName: string
}
constructor (serverContext: ServerContext) {
super(serverContext) super(serverContext)
this.cache = new LRU() this.cache = new LRU()
@ -188,7 +199,7 @@ export default class SPARenderer extends BaseRenderer {
} }
} }
static getPreloadType (ext) { static getPreloadType (ext: string) {
if (ext === 'js') { if (ext === 'js') {
return 'script' return 'script'
} else if (ext === 'css') { } else if (ext === 'css') {

View File

@ -3,13 +3,15 @@ import crypto from 'crypto'
import { format } from 'util' import { format } from 'util'
import fs from 'fs-extra' import fs from 'fs-extra'
import consola from 'consola' import consola from 'consola'
import { TARGETS, urlJoin } from 'nuxt/utils'
import devalue from '@nuxt/devalue' import devalue from '@nuxt/devalue'
import { createBundleRenderer } from 'vue-bundle-renderer' import { createBundleRenderer } from 'vue-bundle-renderer'
import { TARGETS, urlJoin } from 'nuxt/utils'
import ServerContext from 'nuxt/server/context'
import BaseRenderer from './base' import BaseRenderer from './base'
export default class SSRRenderer extends BaseRenderer { export default class SSRRenderer extends BaseRenderer {
constructor (serverContext) { constructor (serverContext: ServerContext) {
super(serverContext) super(serverContext)
this.createRenderer() this.createRenderer()
} }