Merge branch '3.x' of github.com:nuxt/nuxt3 into 3.x

This commit is contained in:
pooya parsa 2020-08-04 12:10:28 +02:00
commit b06346d0df
51 changed files with 661 additions and 258 deletions

View File

@ -9,6 +9,7 @@ import hash from 'hash-sum'
import pify from 'pify'
import upath from 'upath'
import semver from 'semver'
import type { RouteLocationRaw } from 'vue-router'
import debounce from 'lodash/debounce'
import omit from 'lodash/omit'
@ -17,6 +18,7 @@ import uniq from 'lodash/uniq'
import uniqBy from 'lodash/uniqBy'
import { BundleBuilder } from 'src/webpack'
import type { Nuxt } from 'src/core'
import {
r,
@ -26,6 +28,7 @@ import {
determineGlobals,
stripWhitespace,
isIndexFileAndFolder,
DeterminedGlobals,
scanRequireTree,
TARGETS,
isFullStatic
@ -37,7 +40,31 @@ import TemplateContext from './context/template'
const glob = pify(Glob)
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.plugins = []
this.options = nuxt.options
@ -51,7 +78,7 @@ export default class Builder {
this.supportedExtensions = ['vue', 'js', ...(this.options.build.additionalExtensions || [])]
// 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
@ -80,7 +107,7 @@ export default class Builder {
this.resolveAppTemplate()
// Create a new bundle builder
this.bundleBuilder = this.getBundleBuilder(bundleBuilder)
this.bundleBuilder = this.getBundleBuilder()
this.ignore = new Ignore({
rootDir: this.options.srcDir,
@ -857,4 +884,4 @@ const STATUS = {
INITIAL: 1,
BUILD_DONE: 2,
BUILDING: 3
}
} as const

View File

@ -1,5 +1,12 @@
import type Builder from '../builder'
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.nuxt = builder.nuxt
this.options = builder.nuxt.options

View File

@ -1,19 +1,23 @@
import hash from 'hash-sum'
import consola from 'consola'
import uniqBy from 'lodash/uniqBy'
import serialize from 'serialize-javascript'
import devalue from '@nuxt/devalue'
import { r, wp, wChunk, serializeFunction, isFullStatic } from 'src/utils'
import type Builder from '../builder'
export default class TemplateContext {
constructor(builder, options) {
templateFiles: string[]
templateVars: any
constructor (builder: Builder, options) {
this.templateFiles = Array.from(builder.template.files)
this.templateVars = {
nuxtOptions: options,
features: options.features,
extensions: options.extensions
.map(ext => ext.replace(/^\./, ''))
.map((ext: string) => ext.replace(/^\./, ''))
.join('|'),
messages: options.messages,
splitChunks: options.build.splitChunks,
@ -62,7 +66,7 @@ export default class TemplateContext {
hash,
r,
wp,
wChunk,
wChunk
},
interpolate: /<%=([\s\S]+?)%>/g
}

View File

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

View File

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

View File

@ -1,36 +1,67 @@
import path from 'path'
import consola from 'consola'
import minimist from 'minimist'
import Hookable from 'hable'
import minimist, { ParsedArgs } from 'minimist'
import Hookable from 'hookable'
import { Nuxt } from 'src/core'
import { Builder } from 'src/builder'
import { Generator } from 'src/generator'
import type { Target } from 'src/utils'
import { name, version } from '../../package.json'
import { forceExit } from './utils'
import { loadNuxtConfig } from './utils/config'
import { indent, foldLines, colorize } from './utils/formatting'
import { startSpaces, optionSpaces, forceExitTimeout } from './utils/constants'
import { Nuxt } from 'src/core'
import { Builder } from 'src/builder'
import { Generator } from 'src/generator'
export interface Command {
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 {
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)
this.addHooks(hooks)
if (!cmd.options) {
cmd.options = {}
}
this.cmd = cmd
this.cmd = cmd as Command & { options: Command['options'] }
this._argv = Array.from(argv)
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()
}
static from (cmd, argv, hooks) {
static from (cmd: Command, argv: NodeJS.Process['argv'], hooks: Hooks) {
if (cmd instanceof NuxtCommand) {
return cmd
}
@ -54,11 +85,11 @@ export default class NuxtCommand extends Hookable {
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.')
}
let cmdError
let cmdError: any
try {
await this.cmd.run(this)
@ -102,7 +133,7 @@ export default class NuxtCommand extends Hookable {
return this._parsedArgv
}
async getNuxtConfig (extraOptions = {}) {
async getNuxtConfig (extraOptions: ExtraOptions = {}) {
// Flag to indicate nuxt is running with CLI (not programmatic)
extraOptions._cli = true
@ -131,7 +162,7 @@ export default class NuxtCommand extends Hookable {
return nuxt
}
async getBuilder (nuxt) {
async getBuilder (nuxt: Nuxt) {
return new Builder(nuxt)
}
@ -160,12 +191,12 @@ export default class NuxtCommand extends Hookable {
}
}
isUserSuppliedArg (option) {
isUserSuppliedArg (option: string) {
return this._argv.includes(`--${option}`) || this._argv.includes(`--no-${option}`)
}
_getDefaultOptionValue (option) {
return typeof option.default === 'function' ? option.default(this.cmd) : option.default
_getDefaultOptionValue<T, Option extends { default: ((cmd: Command) => T) | T }>(option: Option) {
return option.default instanceof Function ? option.default(this.cmd) : option.default
}
_getMinimistOptions () {

View File

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

View File

@ -1,6 +1,11 @@
import consola from 'consola'
import chalk from 'chalk'
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 { eventsMapping, formatPath } from '../utils'
import { showBanner } from '../utils/banner'
@ -20,13 +25,13 @@ export default {
}
},
async run (cmd) {
async run (cmd: NuxtCommand) {
const { argv } = cmd
await this.startDev(cmd, argv, argv.open)
},
async startDev (cmd, argv) {
async startDev (cmd: NuxtCommand, argv) {
let nuxt
try {
nuxt = await this._listenDev(cmd, argv)
@ -45,7 +50,7 @@ export default {
return nuxt
},
async _listenDev (cmd, argv) {
async _listenDev (cmd: NuxtCommand, argv: ParsedArgs) {
const config = await cmd.getNuxtConfig({ dev: true, _build: true })
const nuxt = await cmd.getNuxt(config)
@ -73,7 +78,7 @@ export default {
return nuxt
},
async _buildDev (cmd, argv, nuxt) {
async _buildDev (cmd: NuxtCommand, _argv: ParsedArgs, nuxt: Nuxt) {
// Create builder instance
const builder = await cmd.getBuilder(nuxt)
@ -92,7 +97,7 @@ export default {
return nuxt
},
logChanged ({ event, path }) {
logChanged ({ event, path }: { event: keyof typeof eventsMapping, path: string }) {
const { icon, color, action } = eventsMapping[event] || eventsMapping.change
consola.log({
@ -110,7 +115,7 @@ export default {
await this.startDev(cmd, argv)
},
onBundlerChange (path) {
onBundlerChange (path: string) {
this.logChanged({ event: 'change', path })
}
}

View File

@ -1,6 +1,7 @@
import path from 'path'
import consola from 'consola'
import { TARGETS } from 'src/utils'
import type NuxtCommand from '../command'
import { common, locking } from '../options'
import { createLock } from '../utils'
@ -17,7 +18,7 @@ export default {
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({
dev: false,
target: TARGETS.static,

View File

@ -1,4 +1,6 @@
import { TARGETS } from 'src/utils'
import type { ParsedArgs } from 'minimist'
import type NuxtCommand from '../command'
import { common, locking } from '../options'
import { normalizeArg, createLock } from '../utils'
@ -18,7 +20,7 @@ export default {
type: 'boolean',
default: false,
description: 'Enable Vue devtools',
prepare (cmd, options, argv) {
prepare (_cmd: NuxtCommand, options, argv: ParsedArgs) {
options.vue = options.vue || {}
options.vue.config = options.vue.config || {}
if (argv.devtools) {
@ -30,7 +32,7 @@ export default {
alias: 'q',
type: 'boolean',
description: 'Disable output except for errors',
prepare (cmd, options, argv) {
prepare (_cmd: NuxtCommand, options, argv: ParsedArgs) {
// Silence output when using --quiet
options.build = options.build || {}
if (argv.quiet) {
@ -41,7 +43,7 @@ export default {
modern: {
...common.modern,
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)) {
options.modern = 'client'
}
@ -53,7 +55,7 @@ export default {
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({
dev: false,
_build: cmd.argv.build,

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import { common, server } from '../options'
import { showBanner } from '../utils/banner'
import { Listener } from 'src/server'
import { Nuxt } from 'src/core'
import type NuxtCommand from '../command'
export default {
name: 'serve',
@ -20,7 +21,7 @@ export default {
help: common.help,
...server
},
async run (cmd) {
async run (cmd: NuxtCommand) {
let options = await cmd.getNuxtConfig({ dev: false })
// add default options
options = getNuxtConfig(options)

View File

@ -1,4 +1,6 @@
import { TARGETS } from 'src/utils'
import type NuxtCommand from '../command'
import { common, server } from '../options'
import { showBanner } from '../utils/banner'
@ -10,7 +12,7 @@ export default {
...common,
...server
},
async run (cmd) {
async run (cmd: NuxtCommand) {
const config = await cmd.getNuxtConfig({ dev: false, _start: true })
if (config.target === TARGETS.static) {
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 consola from 'consola'
import get from 'lodash/get'
import type NuxtCommand from '../command'
import { common } from '../options'
export default {
@ -32,7 +34,7 @@ export default {
description: 'Inspect development mode webpack config'
}
},
async run (cmd) {
async run (cmd: NuxtCommand) {
const { name } = cmd.argv
const queries = [...cmd.argv._]

View File

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

View File

@ -5,7 +5,7 @@ import NuxtCommand from './command'
import setup from './setup'
import getCommand from './commands'
function packageExists (name) {
function packageExists (name: string) {
try {
require.resolve(name)
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
const dupPkg = '@nuxt/' + (pkgName === '@nuxt/cli-edge' ? 'cli' : 'cli-edge')
if (packageExists(dupPkg)) {
@ -25,7 +25,7 @@ export default async function run(_argv, hooks = {}) {
const argv = _argv ? Array.from(_argv) : process.argv.slice(2)
// 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
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
export default function setup ({ dev }) {
export default function setup ({ dev }: { dev: boolean }) {
// Apply default NODE_ENV if not provided
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = dev ? 'development' : 'production'

View File

@ -1,10 +1,12 @@
import consola from 'consola'
import env from 'std-env'
import chalk from 'chalk'
import { Nuxt } from 'nuxt/core'
import { successBox } from './formatting'
import { getFormattedMemoryUsage } from './memory'
export function showBanner (nuxt, showMemoryUsage = true) {
export function showBanner (nuxt: Nuxt, showMemoryUsage = true) {
if (env.test) {
return
}
@ -23,7 +25,7 @@ export function showBanner (nuxt, showMemoryUsage = true) {
const { bannerColor, badgeMessages } = nuxt.options.cli
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
const isDev = nuxt.options.dev

View File

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

View File

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

View File

@ -12,7 +12,7 @@ export const eventsMapping = {
unlink: { icon: '-', color: 'red', action: 'Removed' }
}
export function formatPath (filePath) {
export function formatPath (filePath: string) {
if (!filePath) {
return
}
@ -27,7 +27,7 @@ export function formatPath (filePath) {
* @param {*} defaultValue
* @returns formatted argument
*/
export function normalizeArg (arg, defaultValue) {
export function normalizeArg (arg: boolean | 'true' | '' | 'false', defaultValue?: boolean) {
switch (arg) {
case 'true': arg = true; break
case '': arg = true; break
@ -37,7 +37,7 @@ export function normalizeArg (arg, defaultValue) {
return arg
}
export function forceExit (cmdName, timeout) {
export function forceExit (cmdName: string, timeout: number | false) {
if (timeout !== false) {
const exitTimeout = setTimeout(() => {
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
// 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)
}

View File

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

View File

@ -1,3 +1,4 @@
import { EnvConfig } from 'nuxt/config/load'
import { loadNuxtConfig } from '../config'
import Nuxt from './nuxt'
@ -8,7 +9,19 @@ const OVERRIDES = {
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
if (typeof loadOptions === 'string') {
loadOptions = { for: loadOptions }

View File

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

View File

@ -1,7 +1,7 @@
import isPlainObject from 'lodash/isPlainObject'
import consola from 'consola'
import Hookable from 'hable'
import Hookable from 'hookable'
import { defineAlias } from 'src/utils'
import { getNuxtConfig } from 'src/config'
@ -13,6 +13,17 @@ import ModuleContainer from './module'
import Resolver from './resolver'
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 = {}) {
super(consola)
@ -103,13 +114,14 @@ export default class Nuxt extends Hookable {
defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen'])
}
async close (callback) {
async close (callback?: () => any | Promise<any>) {
await this.callHook('close', this)
if (typeof callback === 'function') {
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 fs from 'fs-extra'
import consola from 'consola'
import { Nuxt } from 'nuxt/core'
import {
startsWithRootAlias,
startsWithSrcAlias,
@ -9,8 +9,25 @@ import {
clearRequireCache
} from 'src/utils'
interface ResolvePathOptions {
isAlias?: boolean
isModule?: boolean
isStyle?: boolean
}
interface RequireModuleOptions {
useESM?: boolean
isAlias?: boolean
interopDefault?: any
}
export default class Resolver {
constructor (nuxt) {
_require: NodeJS.Require
_resolve: RequireResolve
nuxt: Nuxt
options: Nuxt['options']
constructor (nuxt: Nuxt) {
this.nuxt = nuxt
this.options = this.nuxt.options
@ -26,7 +43,7 @@ export default class Resolver {
this._resolve = require.resolve
}
resolveModule (path) {
resolveModule (path: string) {
try {
return this._resolve(path, {
paths: this.options.modulesDir
@ -42,7 +59,7 @@ export default class Resolver {
}
}
resolveAlias (path) {
resolveAlias (path: string) {
if (startsWithRootAlias(path)) {
return join(this.options.rootDir, path.substr(2))
}
@ -54,21 +71,13 @@ export default class Resolver {
return resolve(this.options.srcDir, path)
}
resolvePath (path, { alias, isAlias = alias, module, isModule = module, isStyle } = {}) {
// 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.')
}
resolvePath (path: string, { isAlias, isModule, isStyle }: ResolvePathOptions = {}) {
// Fast return in case of path exists
if (fs.existsSync(path)) {
return path
}
let resolvedPath
let resolvedPath: string
// Try to resolve it as a regular module
if (isModule !== false) {
@ -85,7 +94,7 @@ export default class Resolver {
resolvedPath = path
}
let isDirectory
let isDirectory: boolean
// Check if resolvedPath exits and is not a directory
if (fs.existsSync(resolvedPath)) {
@ -119,22 +128,11 @@ export default class Resolver {
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 requiredModule
let requiredModule: any
// TODO: Remove in Nuxt 3
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
let lastError: any
// Try to resolve path
try {

View File

@ -6,10 +6,32 @@ import defu from 'defu'
import htmlMinifier from 'html-minifier'
import { parse } from 'node-html-parser'
import type { Builder } from 'src/builder'
import type { Nuxt } from 'src/core'
import { isFullStatic, flatRoutes, isString, isUrl, promisifyRoute, waitFor, TARGETS } from 'src/utils'
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.options = nuxt.options
this.builder = builder

View File

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

View File

@ -1,5 +1,12 @@
import { Server } from 'nuxt/server'
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.globals = server.globals
this.options = server.options

View File

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

View File

@ -1,5 +1,6 @@
import http from 'http'
import https from 'https'
import type { ListenOptions } from 'net'
import enableDestroy from 'server-destroy'
import ip from 'ip'
import consola from 'consola'
@ -8,6 +9,19 @@ import pify from 'pify'
let RANDOM_PORT = '0'
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 }) {
// Options
this.port = port
@ -43,6 +57,9 @@ export default class Listener {
computeURL () {
const address = this.server.address()
if (typeof address === 'string') {
return address
}
if (!this.socket) {
switch (address.address) {
case '127.0.0.1': this.host = 'localhost'; break
@ -68,7 +85,7 @@ export default class Listener {
// Call server.listen
// 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
// Call server.listen

View File

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

View File

@ -1,10 +1,14 @@
import path from 'path'
import { ServerResponse } from 'http'
import consola from 'consola'
import launchMiddleware from 'launch-editor-middleware'
import serveStatic from 'serve-static'
import servePlaceholder from 'serve-placeholder'
import connect from 'connect'
import { determineGlobals, isUrl } from 'src/utils'
import connect, { IncomingMessage } from 'connect'
import type { TemplateExecutor } from 'lodash'
import { Nuxt } from 'src/core'
import { DeterminedGlobals, determineGlobals, isUrl } from 'src/utils'
import { VueRenderer } from 'src/vue-renderer'
import ServerContext from './context'
@ -14,8 +18,34 @@ import errorMiddleware from './middleware/error'
import Listener from './listener'
import createTimingMiddleware from './middleware/timing'
interface Manifest {
assetsMapping: Record<string, string[]>
publicPath: string
}
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.options = nuxt.options
@ -76,7 +106,7 @@ export default class Server {
const { compressor } = this.options.render
if (typeof compressor === 'object') {
// 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))
} else if (compressor) {
// 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)
}
renderRoute () {
return this.renderer.renderRoute.apply(this.renderer, arguments)
renderRoute (...args: Parameters<VueRenderer['renderRoute']>) {
return this.renderer.renderRoute.apply(this.renderer, ...args.slice())
}
loadResources () {
return this.renderer.loadResources.apply(this.renderer, arguments)
loadResources (...args: Parameters<VueRenderer['loadResources']>) {
return this.renderer.loadResources.apply(this.renderer, ...args)
}
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
await this.nuxt.ready()
// Create a 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,
socket: socket || this.options.server.socket,
https: this.options.server.https,

View File

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

View File

@ -1,9 +1,13 @@
export const TARGETS = {
server: 'server',
static: 'static'
}
} as const
export type Target = keyof typeof TARGETS
export const MODES = {
universal: 'universal',
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'
export const getContext = function getContext (req, res) {
export const getContext = function getContext (req: IncomingMessage, res: ServerResponse) {
return { req, res }
}
export const determineGlobals = function determineGlobals (globalName, globals) {
const _globals = {}
type NuxtGlobal = string | ((globalName: string) => string)
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) {
if (typeof globals[global] === 'function') {
_globals[global] = globals[global](globalName)
const currentGlobal = globals[global]
if (currentGlobal instanceof Function) {
_globals[global] = currentGlobal(globalName)
} else {
_globals[global] = globals[global]
_globals[global] = currentGlobal
}
}
return _globals
return _globals as DeterminedGlobals
}
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;')
}
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))
}
export const urlJoin = function urlJoin () {
export const urlJoin = function urlJoin (...args: string[]) {
return [].slice
.call(arguments)
.call(args)
.join('/')
.replace(/\/+/g, '/')
.replace(':/', '://')
@ -22,13 +27,11 @@ export const urlJoin = function urlJoin () {
/**
* 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
[/{\n{2,}/g, '{\n'], // strip start 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)
]
export const stripWhitespace = function stripWhitespace (string) {
export const stripWhitespace = function stripWhitespace (string: string) {
WHITESPACE_REPLACEMENTS.forEach(([regex, newSubstr]) => {
string = string.replace(regex, newSubstr)
})

View File

@ -2,27 +2,42 @@ import path from 'path'
import consola from 'consola'
import hash from 'hash-sum'
import fs from 'fs-extra'
import properlock from 'proper-lockfile'
import properlock, { LockOptions } from 'proper-lockfile'
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,
onCompromised: err => consola.warn(err)
}
export function getLockOptions (options) {
export function getLockOptions (options: Partial<LockOptions>) {
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}`)
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)
// the lock is created for the lockPath as ${lockPath}.lock
@ -32,8 +47,12 @@ export async function getLockPath (config) {
return lockPath
}
export async function lock ({ id, dir, root, options }) {
const lockPath = await getLockPath({ id, dir, root })
export async function lock ({ id, dir, root, options }: NuxtLockOptions) {
const lockPath = await getLockPath({
id,
dir,
root
})
try {
const locked = await properlock.check(lockPath)
@ -45,7 +64,7 @@ export async function lock ({ id, dir, root, options }) {
}
let lockWasCompromised = false
let release
let release: (() => Promise<void>) | undefined
try {
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
// compromised due to mtime update timeouts
const lockDir = `${lockPath}.lock`
if (await fs.exists(lockDir)) {
if (await fs.pathExists(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 = {
Edge: '16',
@ -12,49 +15,65 @@ export const ModernBrowsers = {
Yandex: '18',
Vivaldi: '1.14',
'Mobile Safari': '10.3'
}
} as const
let semver
let __modernBrowsers
type ModernBrowsers = { -readonly [key in keyof typeof ModernBrowsers]: SemVer }
let semver: typeof import('semver')
let __modernBrowsers: ModernBrowsers
const getModernBrowsers = () => {
if (__modernBrowsers) {
return __modernBrowsers
}
__modernBrowsers = Object.keys(ModernBrowsers)
.reduce((allBrowsers, browser) => {
allBrowsers[browser] = semver.coerce(ModernBrowsers[browser])
__modernBrowsers = (Object.keys(ModernBrowsers) as Array<
keyof typeof ModernBrowsers
>).reduce(
(allBrowsers, browser) => {
const version = semver.coerce(ModernBrowsers[browser])
if (version) { allBrowsers[browser] = version }
return allBrowsers
}, {})
},
{} as ModernBrowsers
)
return __modernBrowsers
}
export const isModernBrowser = (ua) => {
interface NuxtRequest extends IncomingMessage {
socket: IncomingMessage['socket'] & {
_modern?: boolean
}
}
export const isModernBrowser = (ua: string) => {
if (!ua) {
return false
}
if (!semver) {
semver = require('semver')
}
const { browser } = UAParser(ua)
const browser = new UAParser(ua).getBrowser()
const browserVersion = semver.coerce(browser.version)
if (!browserVersion) {
return false
}
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) {
return false
}
const { socket = {}, headers } = req
const { socket = {} as NuxtRequest['socket'], headers } = req
if (socket._modern === undefined) {
const ua = headers && headers['user-agent']
socket._modern = isModernBrowser(ua)
socket._modern = ua && isModernBrowser(ua)
}
return socket._modern

View File

@ -2,7 +2,8 @@ import path from 'path'
import consola from 'consola'
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(['@', '~'])
@ -24,9 +25,9 @@ export const wChunk = function wChunk (p = '') {
const reqSep = /\//g
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]
if (startsWithSrcAlias(lastArg)) {
@ -36,14 +37,12 @@ export const r = function r (...args) {
return wp(path.resolve(...args.map(normalize)))
}
export const relativeTo = function relativeTo (...args) {
const dir = args.shift()
export const relativeTo = function relativeTo (dir: string, ...args: string[]): string {
// Keep webpack inline loader intact
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
@ -63,7 +62,17 @@ export const relativeTo = function relativeTo (...args) {
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
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)
if (pluginFiles.length !== 2) {
return false
@ -105,5 +114,9 @@ export function isIndexFileAndFolder (pluginFiles) {
}
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 consola from 'consola'
import type { Component } from 'vue'
import type { _RouteRecordBase } from 'vue-router'
import { r } from './resolve'
export const flatRoutes = function flatRoutes (router, fileName = '', routes = []) {
export const flatRoutes = function flatRoutes (router, fileName = '', routes: string[] = []) {
router.forEach((r) => {
if ([':', '*'].some(c => r.path.includes(c))) {
return
@ -30,12 +33,12 @@ export const flatRoutes = function flatRoutes (router, fileName = '', routes = [
return routes
}
function cleanChildrenRoutes (routes, isChild = false, routeNameSplitter = '-') {
function cleanChildrenRoutes (routes: NuxtRouteConfig[], isChild = false, routeNameSplitter = '-') {
let start = -1
const regExpIndex = new RegExp(`${routeNameSplitter}index$`)
const routesIndex = []
const routesIndex: string[][] = []
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
const res = route.name.split(routeNameSplitter)
const s = res.indexOf('index')
@ -46,7 +49,7 @@ function cleanChildrenRoutes (routes, isChild = false, routeNameSplitter = '-')
routes.forEach((route) => {
route.path = isChild ? route.path.replace('/', '') : route.path
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('/')
if (!isChild) {
paths.shift()
@ -66,7 +69,9 @@ function cleanChildrenRoutes (routes, isChild = false, routeNameSplitter = '-')
})
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.find(child => child.path === '')) {
delete route.name
@ -79,7 +84,7 @@ function cleanChildrenRoutes (routes, isChild = false, routeNameSplitter = '-')
const DYNAMIC_ROUTE_REGEX = /^\/([:*])/
export const sortRoutes = function sortRoutes (routes) {
export const sortRoutes = function sortRoutes (routes: NuxtRouteConfig[]) {
routes.sort((a, b) => {
if (!a.path.length) {
return -1
@ -136,6 +141,22 @@ export const sortRoutes = function sortRoutes (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 ({
files,
srcDir,
@ -143,8 +164,8 @@ export const createRoutes = function createRoutes ({
routeNameSplitter = '-',
supportedExtensions = ['vue', 'js'],
trailingSlash
}) {
const routes = []
}: CreateRouteOptions) {
const routes: NuxtRouteConfig[] = []
files.forEach((file) => {
const keys = file
.replace(new RegExp(`^${pagesDir}`), '')
@ -152,13 +173,13 @@ export const createRoutes = function createRoutes ({
.replace(/\/{2,}/g, '/')
.split('/')
.slice(1)
const route = { name: '', path: '', component: r(srcDir, file) }
const route: NuxtRouteConfig = { name: '', path: '', component: r(srcDir, file) }
let parent = routes
keys.forEach((key, i) => {
// remove underscore only, if its the prefix
const sanitizedKey = key.startsWith('_') ? key.substr(1) : key
route.name = route.name
route.name = route.name && typeof route.name === 'string'
? route.name + routeNameSplitter + sanitizedKey
: sanitizedKey
route.name += key === '_' ? 'all' : ''
@ -192,9 +213,9 @@ export const createRoutes = function createRoutes ({
}
// Guard dir1 from dir2 which can be indiscriminately removed
export const guardDir = function guardDir (options, key1, key2) {
const dir1 = get(options, key1, false)
const dir2 = get(options, key2, false)
export const guardDir = function guardDir (options: Record<string, unknown>, key1: string, key2: string) {
const dir1 = get(options, key1, false) as string
const dir2 = get(options, key2, false) as string
if (
dir1 &&
@ -213,7 +234,7 @@ export const guardDir = function guardDir (options, key1, key2) {
}
}
const getRoutePathExtension = (key) => {
const getRoutePathExtension = (key: string) => {
if (key === '_') {
return '*'
}

View File

@ -1,6 +1,10 @@
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) {
return obj
}
@ -22,14 +26,14 @@ export function normalizeFunctions (obj) {
functionBody = `return ${functionBody}`
}
// 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
}
export function serializeFunction (func) {
export function serializeFunction (func: Function) {
let open = false
func = normalizeFunctions(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(
(promise, task) => promise.then(() => fn(task)),
(promise, task): any => promise.then(() => fn(task)),
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))
}

View File

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

View File

@ -7,9 +7,22 @@ import { TARGETS, isModernRequest, waitFor } from 'src/utils'
import SPARenderer from './renderers/spa'
import SSRRenderer from './renderers/ssr'
import ModernRenderer from './renderers/modern'
import ServerContext from 'nuxt/server/context'
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.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 readResource = async (fileName, encoding) => {
const readResource = async (fileName: string, encoding: string) => {
try {
const fullPath = path.resolve(this.distPath, fileName)
@ -311,16 +324,16 @@ export default class VueRenderer {
return {
clientManifest: {
fileName: 'client.manifest.json',
transform: src => JSON.parse(src)
transform: (src: string) => JSON.parse(src)
},
modernManifest: {
fileName: 'modern.manifest.json',
transform: src => JSON.parse(src)
transform: (src: string) => JSON.parse(src)
},
serverManifest: {
fileName: 'server.manifest.json',
// BundleRenderer needs resolved contents
transform: async (src, { readResource }) => {
transform: async (src: string, { readResource }) => {
const serverManifest = JSON.parse(src)
const readResources = async (obj) => {
@ -357,16 +370,16 @@ export default class VueRenderer {
},
ssrTemplate: {
fileName: 'index.ssr.html',
transform: src => this.parseTemplate(src)
transform: (src: string) => this.parseTemplate(src)
},
spaTemplate: {
fileName: 'index.spa.html',
transform: src => this.parseTemplate(src)
transform: (src: string) => this.parseTemplate(src)
}
}
}
parseTemplate (templateStr) {
parseTemplate (templateStr: string) {
return template(templateStr, {
interpolate: /{{([\s\S]+?)}}/g,
evaluate: /{%([\s\S]+?)%}/g

View File

@ -1,10 +1,15 @@
import ServerContext from "nuxt/server/context"
export default class BaseRenderer {
constructor (serverContext) {
serverContext: ServerContext
options: ServerContext['options']
constructor (serverContext: ServerContext) {
this.serverContext = serverContext
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)
opts.html_attrs = opts.HTML_ATTRS
opts.head_attrs = opts.HEAD_ATTRS
@ -13,7 +18,7 @@ export default class BaseRenderer {
return templateFn(opts)
}
render () {
render (renderContext) {
throw new Error('`render()` needs to be implemented')
}
}

View File

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

View File

@ -4,10 +4,20 @@ import VueMeta from 'vue-meta'
import LRU from 'lru-cache'
import devalue from '@nuxt/devalue'
import { TARGETS, isModernRequest } from 'src/utils'
import ServerContext from 'src/server/context'
import BaseRenderer from './base'
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)
this.cache = new LRU()
@ -188,7 +198,7 @@ export default class SPARenderer extends BaseRenderer {
}
}
static getPreloadType (ext) {
static getPreloadType (ext: string) {
if (ext === 'js') {
return 'script'
} else if (ext === 'css') {

View File

@ -6,10 +6,12 @@ import consola from 'consola'
import { TARGETS, urlJoin } from 'src/utils'
import devalue from '@nuxt/devalue'
import { createBundleRenderer } from 'vue-bundle-renderer'
import ServerContext from 'src/server/context'
import BaseRenderer from './base'
export default class SSRRenderer extends BaseRenderer {
constructor (serverContext) {
constructor (serverContext: ServerContext) {
super(serverContext)
this.createRenderer()
}