mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25:11 +00:00
initial commit
This commit is contained in:
commit
14f187e69b
175
packages/nuxt3/src/babel-preset-app/index.js
Normal file
175
packages/nuxt3/src/babel-preset-app/index.js
Normal file
@ -0,0 +1,175 @@
|
||||
const coreJsMeta = {
|
||||
2: {
|
||||
prefixes: {
|
||||
es6: 'es6',
|
||||
es7: 'es7'
|
||||
},
|
||||
builtIns: '@babel/compat-data/corejs2-built-ins'
|
||||
},
|
||||
3: {
|
||||
prefixes: {
|
||||
es6: 'es',
|
||||
es7: 'es'
|
||||
},
|
||||
builtIns: 'core-js-compat/data'
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultPolyfills (corejs) {
|
||||
const { prefixes: { es6, es7 } } = coreJsMeta[corejs.version]
|
||||
return [
|
||||
// Promise polyfill alone doesn't work in IE,
|
||||
// Needs this as well. see: #1642
|
||||
`${es6}.array.iterator`,
|
||||
// This is required for webpack code splitting, vuex etc.
|
||||
`${es6}.promise`,
|
||||
// this is needed for object rest spread support in templates
|
||||
// as vue-template-es2015-compiler 1.8+ compiles it to Object.assign() calls.
|
||||
`${es6}.object.assign`,
|
||||
// #2012 es7.promise replaces native Promise in FF and causes missing finally
|
||||
`${es7}.promise.finally`
|
||||
]
|
||||
}
|
||||
|
||||
function getPolyfills (targets, includes, { ignoreBrowserslistConfig, configPath, corejs }) {
|
||||
const { default: getTargets, isRequired } = require('@babel/helper-compilation-targets')
|
||||
const builtInsList = require(coreJsMeta[corejs.version].builtIns)
|
||||
const builtInTargets = getTargets(targets, {
|
||||
ignoreBrowserslistConfig,
|
||||
configPath
|
||||
})
|
||||
|
||||
return includes.filter(item => isRequired(
|
||||
'nuxt-polyfills',
|
||||
builtInTargets,
|
||||
{
|
||||
compatData: { 'nuxt-polyfills': builtInsList[item] }
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
function isPackageHoisted (packageName) {
|
||||
const path = require('path')
|
||||
const installedPath = require.resolve(packageName)
|
||||
const relativePath = path.resolve(__dirname, '..', 'node_modules', packageName)
|
||||
return installedPath !== relativePath
|
||||
}
|
||||
|
||||
module.exports = (api, options = {}) => {
|
||||
const presets = []
|
||||
const plugins = []
|
||||
|
||||
const envName = api.env()
|
||||
|
||||
const {
|
||||
bugfixes,
|
||||
polyfills: userPolyfills,
|
||||
loose = false,
|
||||
debug = false,
|
||||
useBuiltIns = 'usage',
|
||||
modules = false,
|
||||
spec,
|
||||
ignoreBrowserslistConfig = envName === 'modern',
|
||||
configPath,
|
||||
include,
|
||||
exclude,
|
||||
shippedProposals,
|
||||
forceAllTransforms,
|
||||
decoratorsBeforeExport,
|
||||
decoratorsLegacy,
|
||||
absoluteRuntime
|
||||
} = options
|
||||
|
||||
let { corejs = { version: 3 } } = options
|
||||
|
||||
if (typeof corejs !== 'object') {
|
||||
corejs = { version: Number(corejs) }
|
||||
}
|
||||
|
||||
const isCorejs3Hoisted = isPackageHoisted('core-js')
|
||||
if (
|
||||
(corejs.version === 3 && !isCorejs3Hoisted) ||
|
||||
(corejs.version === 2 && isCorejs3Hoisted)
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
(console.fatal || console.error)(`babel corejs option is ${corejs.version}, please directlly install core-js@${corejs.version}.`)
|
||||
}
|
||||
|
||||
const defaultTargets = {
|
||||
server: { node: 'current' },
|
||||
client: { ie: 9 },
|
||||
modern: { esmodules: true }
|
||||
}
|
||||
|
||||
let { targets = defaultTargets[envName] } = options
|
||||
|
||||
// modern mode can only be { esmodules: true }
|
||||
if (envName === 'modern') {
|
||||
targets = defaultTargets.modern
|
||||
}
|
||||
|
||||
const polyfills = []
|
||||
|
||||
if (envName === 'client' && useBuiltIns === 'usage') {
|
||||
polyfills.push(
|
||||
...getPolyfills(
|
||||
targets,
|
||||
userPolyfills || getDefaultPolyfills(corejs),
|
||||
{
|
||||
ignoreBrowserslistConfig,
|
||||
configPath,
|
||||
corejs
|
||||
}
|
||||
)
|
||||
)
|
||||
plugins.push([require('./polyfills-plugin'), { polyfills }])
|
||||
}
|
||||
|
||||
// Pass options along to babel-preset-env
|
||||
presets.push([
|
||||
require('@babel/preset-env'), {
|
||||
bugfixes,
|
||||
spec,
|
||||
loose,
|
||||
debug,
|
||||
modules,
|
||||
targets,
|
||||
useBuiltIns,
|
||||
corejs,
|
||||
ignoreBrowserslistConfig,
|
||||
configPath,
|
||||
include,
|
||||
exclude: polyfills.concat(exclude || []),
|
||||
shippedProposals,
|
||||
forceAllTransforms
|
||||
}
|
||||
])
|
||||
|
||||
// JSX
|
||||
if (options.jsx !== false) {
|
||||
// presets.push([require('@vue/babel-preset-jsx'), Object.assign({}, options.jsx)])
|
||||
}
|
||||
|
||||
plugins.push(
|
||||
[require('@babel/plugin-proposal-decorators'), {
|
||||
decoratorsBeforeExport,
|
||||
legacy: decoratorsLegacy !== false
|
||||
}],
|
||||
[require('@babel/plugin-proposal-class-properties'), { loose: true }]
|
||||
)
|
||||
|
||||
// Transform runtime, but only for helpers
|
||||
plugins.push([require('@babel/plugin-transform-runtime'), {
|
||||
regenerator: useBuiltIns !== 'usage',
|
||||
corejs: false,
|
||||
helpers: useBuiltIns === 'usage',
|
||||
useESModules: envName !== 'server',
|
||||
absoluteRuntime
|
||||
}])
|
||||
|
||||
return {
|
||||
sourceType: 'unambiguous',
|
||||
presets,
|
||||
plugins
|
||||
}
|
||||
}
|
24
packages/nuxt3/src/babel-preset-app/polyfills-plugin.js
Normal file
24
packages/nuxt3/src/babel-preset-app/polyfills-plugin.js
Normal file
@ -0,0 +1,24 @@
|
||||
// Add polyfill imports to the first file encountered.
|
||||
module.exports = ({ types }) => {
|
||||
let entryFile
|
||||
return {
|
||||
name: 'inject-polyfills',
|
||||
visitor: {
|
||||
Program (path, state) {
|
||||
if (!entryFile) {
|
||||
entryFile = state.filename
|
||||
} else if (state.filename !== entryFile) {
|
||||
return
|
||||
}
|
||||
|
||||
const { polyfills } = state.opts
|
||||
const { createImport } = require('@babel/preset-env/lib/utils')
|
||||
|
||||
// Imports are injected in reverse order
|
||||
polyfills.slice().reverse().forEach((p) => {
|
||||
createImport(path, p)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
849
packages/nuxt3/src/builder/builder.ts
Normal file
849
packages/nuxt3/src/builder/builder.ts
Normal file
@ -0,0 +1,849 @@
|
||||
import path from 'path'
|
||||
import chalk from 'chalk'
|
||||
import chokidar from 'chokidar'
|
||||
import consola from 'consola'
|
||||
import fsExtra from 'fs-extra'
|
||||
import Glob from 'glob'
|
||||
import hash from 'hash-sum'
|
||||
import pify from 'pify'
|
||||
import upath from 'upath'
|
||||
import semver from 'semver'
|
||||
|
||||
import debounce from 'lodash/debounce'
|
||||
import omit from 'lodash/omit'
|
||||
import template from 'lodash/template'
|
||||
import uniq from 'lodash/uniq'
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
|
||||
import { BundleBuilder } from 'src/webpack'
|
||||
import vueAppTemplate from 'src/vue-app/template'
|
||||
|
||||
import {
|
||||
r,
|
||||
createRoutes,
|
||||
relativeTo,
|
||||
waitFor,
|
||||
determineGlobals,
|
||||
stripWhitespace,
|
||||
isIndexFileAndFolder,
|
||||
scanRequireTree,
|
||||
TARGETS,
|
||||
isFullStatic
|
||||
} from 'src/utils'
|
||||
|
||||
import Ignore from './ignore'
|
||||
import BuildContext from './context/build'
|
||||
import TemplateContext from './context/template'
|
||||
|
||||
const glob = pify(Glob)
|
||||
export default class Builder {
|
||||
constructor (nuxt, bundleBuilder) {
|
||||
this.nuxt = nuxt
|
||||
this.plugins = []
|
||||
this.options = nuxt.options
|
||||
this.globals = determineGlobals(nuxt.options.globalName, nuxt.options.globals)
|
||||
this.watchers = {
|
||||
files: null,
|
||||
custom: null,
|
||||
restart: null
|
||||
}
|
||||
|
||||
this.supportedExtensions = ['vue', 'js', ...(this.options.build.additionalExtensions || [])]
|
||||
|
||||
// Helper to resolve build paths
|
||||
this.relativeToBuild = (...args) => relativeTo(this.options.buildDir, ...args)
|
||||
|
||||
this._buildStatus = STATUS.INITIAL
|
||||
|
||||
// Hooks for watch lifecycle
|
||||
if (this.options.dev) {
|
||||
// Start watching after initial render
|
||||
this.nuxt.hook('build:done', () => {
|
||||
consola.info('Waiting for file changes')
|
||||
this.watchClient()
|
||||
this.watchRestart()
|
||||
})
|
||||
|
||||
// Enable HMR for serverMiddleware
|
||||
this.serverMiddlewareHMR()
|
||||
|
||||
// Close hook
|
||||
this.nuxt.hook('close', () => this.close())
|
||||
}
|
||||
|
||||
if (this.options.build.analyze) {
|
||||
this.nuxt.hook('build:done', () => {
|
||||
consola.warn('Notice: Please do not deploy bundles built with "analyze" mode, they\'re for analysis purposes only.')
|
||||
})
|
||||
}
|
||||
|
||||
// Resolve template
|
||||
this.template = vueAppTemplate
|
||||
|
||||
// Create a new bundle builder
|
||||
this.bundleBuilder = this.getBundleBuilder(bundleBuilder)
|
||||
|
||||
this.ignore = new Ignore({
|
||||
rootDir: this.options.srcDir,
|
||||
ignoreArray: this.options.ignore
|
||||
})
|
||||
|
||||
// Add support for App.{ext} (or app.{ext})
|
||||
this.appFiles = []
|
||||
for (const ext of this.supportedExtensions) {
|
||||
for (const name of ['app', 'App']) {
|
||||
this.appFiles.push(path.join(this.options.srcDir, `${name}.${ext}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getBundleBuilder () {
|
||||
const context = new BuildContext(this)
|
||||
return new BundleBuilder(context)
|
||||
}
|
||||
|
||||
forGenerate () {
|
||||
this.options.target = TARGETS.static
|
||||
this.bundleBuilder.forGenerate()
|
||||
}
|
||||
|
||||
async build () {
|
||||
// Avoid calling build() method multiple times when dev:true
|
||||
if (this._buildStatus === STATUS.BUILD_DONE && this.options.dev) {
|
||||
return this
|
||||
}
|
||||
// If building
|
||||
if (this._buildStatus === STATUS.BUILDING) {
|
||||
await waitFor(1000)
|
||||
return this.build()
|
||||
}
|
||||
this._buildStatus = STATUS.BUILDING
|
||||
|
||||
if (this.options.dev) {
|
||||
consola.info('Preparing project for development')
|
||||
consola.info('Initial build may take a while')
|
||||
} else {
|
||||
consola.info('Production build')
|
||||
if (this.options.render.ssr) {
|
||||
consola.info(`Bundling for ${chalk.bold.yellow('server')} and ${chalk.bold.green('client')} side`)
|
||||
} else {
|
||||
consola.info(`Bundling only for ${chalk.bold.green('client')} side`)
|
||||
}
|
||||
const target = isFullStatic(this.options) ? 'full static' : this.options.target
|
||||
consola.info(`Target: ${chalk.bold.cyan(target)}`)
|
||||
}
|
||||
|
||||
// Wait for nuxt ready
|
||||
await this.nuxt.ready()
|
||||
|
||||
// Call before hook
|
||||
await this.nuxt.callHook('build:before', this, this.options.build)
|
||||
|
||||
// await this.validatePages()
|
||||
|
||||
// Validate template
|
||||
try {
|
||||
this.validateTemplate()
|
||||
} catch (err) {
|
||||
consola.fatal(err)
|
||||
}
|
||||
|
||||
consola.success('Builder initialized')
|
||||
|
||||
consola.debug(`App root: ${this.options.srcDir}`)
|
||||
|
||||
// Create or empty .nuxt/, .nuxt/components and .nuxt/dist folders
|
||||
await fsExtra.emptyDir(r(this.options.buildDir))
|
||||
const buildDirs = [r(this.options.buildDir, 'components')]
|
||||
if (!this.options.dev) {
|
||||
buildDirs.push(
|
||||
r(this.options.buildDir, 'dist', 'client'),
|
||||
r(this.options.buildDir, 'dist', 'server')
|
||||
)
|
||||
}
|
||||
await Promise.all(buildDirs.map(dir => fsExtra.emptyDir(dir)))
|
||||
|
||||
// Call ready hook
|
||||
await this.nuxt.callHook('builder:prepared', this, this.options.build)
|
||||
|
||||
// Generate routes and interpret the template files
|
||||
await this.generateRoutesAndFiles()
|
||||
|
||||
// Add vue-app template dir to watchers
|
||||
this.options.build.watch.push(this.globPathWithExtensions(this.template.dir))
|
||||
|
||||
await this.resolvePlugins()
|
||||
|
||||
// Start bundle build: webpack, rollup, parcel...
|
||||
await this.bundleBuilder.build()
|
||||
|
||||
// Flag to set that building is done
|
||||
this._buildStatus = STATUS.BUILD_DONE
|
||||
|
||||
// Call done hook
|
||||
await this.nuxt.callHook('build:done', this)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
// Check if pages dir exists and warn if not
|
||||
// async validatePages () {
|
||||
// this._nuxtPages = typeof this.options.build.createRoutes !== 'function'
|
||||
|
||||
// if (
|
||||
// !this._nuxtPages ||
|
||||
// await fsExtra.exists(path.join(this.options.srcDir, this.options.dir.pages))
|
||||
// ) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// const dir = this.options.srcDir
|
||||
// if (await fsExtra.exists(path.join(this.options.srcDir, '..', this.options.dir.pages))) {
|
||||
// throw new Error(
|
||||
// `No \`${this.options.dir.pages}\` directory found in ${dir}. Did you mean to run \`nuxt\` in the parent (\`../\`) directory?`
|
||||
// )
|
||||
// }
|
||||
|
||||
// this._defaultPage = true
|
||||
// consola.warn(`No \`${this.options.dir.pages}\` directory found in ${dir}. Using the default built-in page.`)
|
||||
// }
|
||||
|
||||
validateTemplate () {
|
||||
// Validate template dependencies
|
||||
const templateDependencies = this.template.dependencies
|
||||
for (const depName in templateDependencies) {
|
||||
const depVersion = templateDependencies[depName]
|
||||
|
||||
// Load installed version
|
||||
const pkg = this.nuxt.resolver.requireModule(path.join(depName, 'package.json'))
|
||||
if (pkg) {
|
||||
const validVersion = semver.satisfies(pkg.version, depVersion)
|
||||
if (!validVersion) {
|
||||
consola.warn(`${depName}@${depVersion} is recommended but ${depName}@${pkg.version} is installed!`)
|
||||
}
|
||||
} else {
|
||||
consola.warn(`${depName}@${depVersion} is required but not installed!`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
globPathWithExtensions (path) {
|
||||
return `${path}/**/*.{${this.supportedExtensions.join(',')}}`
|
||||
}
|
||||
|
||||
createTemplateContext () {
|
||||
return new TemplateContext(this, this.options)
|
||||
}
|
||||
|
||||
async generateRoutesAndFiles () {
|
||||
consola.debug('Generating nuxt files')
|
||||
|
||||
this.plugins = Array.from(await this.normalizePlugins())
|
||||
|
||||
const templateContext = this.createTemplateContext()
|
||||
|
||||
await this.resolvePages(templateContext)
|
||||
await this.resolveApp(templateContext)
|
||||
|
||||
await Promise.all([
|
||||
this.resolveLayouts(templateContext),
|
||||
this.resolveStore(templateContext),
|
||||
this.resolveMiddleware(templateContext)
|
||||
])
|
||||
|
||||
await this.resolvePlugins(templateContext)
|
||||
|
||||
this.addOptionalTemplates(templateContext)
|
||||
|
||||
await this.resolveCustomTemplates(templateContext)
|
||||
|
||||
await this.resolveLoadingIndicator(templateContext)
|
||||
|
||||
await this.compileTemplates(templateContext)
|
||||
|
||||
consola.success('Nuxt files generated')
|
||||
}
|
||||
|
||||
async normalizePlugins () {
|
||||
// options.extendPlugins allows for returning a new plugins array
|
||||
if (typeof this.options.extendPlugins === 'function') {
|
||||
const extendedPlugins = this.options.extendPlugins(this.options.plugins)
|
||||
|
||||
if (Array.isArray(extendedPlugins)) {
|
||||
this.options.plugins = extendedPlugins
|
||||
}
|
||||
}
|
||||
|
||||
// extendPlugins hook only supports in-place modifying
|
||||
await this.nuxt.callHook('builder:extendPlugins', this.options.plugins)
|
||||
|
||||
const modes = ['client', 'server']
|
||||
const modePattern = new RegExp(`\\.(${modes.join('|')})(\\.\\w+)*$`)
|
||||
return uniqBy(
|
||||
this.options.plugins.map((p) => {
|
||||
if (typeof p === 'string') {
|
||||
p = { src: p }
|
||||
}
|
||||
const pluginBaseName = path.basename(p.src, path.extname(p.src)).replace(
|
||||
/[^a-zA-Z?\d\s:]/g,
|
||||
''
|
||||
)
|
||||
|
||||
if (p.ssr === false) {
|
||||
p.mode = 'client'
|
||||
} else if (p.mode === undefined) {
|
||||
p.mode = 'all'
|
||||
p.src.replace(modePattern, (_, mode) => {
|
||||
if (modes.includes(mode)) {
|
||||
p.mode = mode
|
||||
}
|
||||
})
|
||||
} else if (!['client', 'server', 'all'].includes(p.mode)) {
|
||||
consola.warn(`Invalid plugin mode (server/client/all): '${p.mode}'. Falling back to 'all'`)
|
||||
p.mode = 'all'
|
||||
}
|
||||
|
||||
return {
|
||||
src: this.nuxt.resolver.resolveAlias(p.src),
|
||||
mode: p.mode,
|
||||
name: 'nuxt_plugin_' + pluginBaseName + '_' + hash(p.src)
|
||||
}
|
||||
}),
|
||||
p => p.name
|
||||
)
|
||||
}
|
||||
|
||||
addOptionalTemplates (templateContext) {
|
||||
if (this.options.build.indicator) {
|
||||
// templateContext.templateFiles.push('components/nuxt-build-indicator.vue')
|
||||
}
|
||||
|
||||
if (this.options.loading !== false) {
|
||||
// templateContext.templateFiles.push('components/nuxt-loading.vue')
|
||||
}
|
||||
}
|
||||
|
||||
async resolveFiles (dir, cwd = this.options.srcDir) {
|
||||
return this.ignore.filter(await glob(this.globPathWithExtensions(dir), {
|
||||
cwd,
|
||||
follow: this.options.build.followSymlinks
|
||||
}))
|
||||
}
|
||||
|
||||
async resolveRelative (dir) {
|
||||
const dirPrefix = new RegExp(`^${dir}/`)
|
||||
return (await this.resolveFiles(dir)).map(file => ({ src: file.replace(dirPrefix, '') }))
|
||||
}
|
||||
|
||||
async resolveApp ({ templateVars }) {
|
||||
templateVars.appPath = 'nuxt-app/app.tutorial.vue'
|
||||
|
||||
for (const appFile of this.appFiles) {
|
||||
if (await fsExtra.exists(appFile)) {
|
||||
templateVars.appPath = appFile
|
||||
templateVars.hasApp = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
templateVars.hasApp = false
|
||||
}
|
||||
|
||||
async resolveLayouts ({ templateVars, templateFiles }) {
|
||||
if (!this.options.features.layouts) {
|
||||
return
|
||||
}
|
||||
|
||||
if (await fsExtra.exists(path.resolve(this.options.srcDir, this.options.dir.layouts))) {
|
||||
for (const file of await this.resolveFiles(this.options.dir.layouts)) {
|
||||
const name = file
|
||||
.replace(new RegExp(`^${this.options.dir.layouts}/`), '')
|
||||
.replace(new RegExp(`\\.(${this.supportedExtensions.join('|')})$`), '')
|
||||
|
||||
// Layout Priority: module.addLayout > .vue file > other extensions
|
||||
if (name === 'error') {
|
||||
if (!templateVars.components.ErrorPage) {
|
||||
templateVars.components.ErrorPage = this.relativeToBuild(
|
||||
this.options.srcDir,
|
||||
file
|
||||
)
|
||||
}
|
||||
} else if (this.options.layouts[name]) {
|
||||
consola.warn(`Duplicate layout registration, "${name}" has been registered as "${this.options.layouts[name]}"`)
|
||||
} else if (!templateVars.layouts[name] || /\.vue$/.test(file)) {
|
||||
templateVars.layouts[name] = this.relativeToBuild(
|
||||
this.options.srcDir,
|
||||
file
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no default layout, create its folder and add the default folder
|
||||
if (!templateVars.layouts.default) {
|
||||
await fsExtra.mkdirp(r(this.options.buildDir, 'layouts'))
|
||||
templateFiles.push('layouts/default.vue')
|
||||
templateVars.layouts.default = './layouts/default.vue'
|
||||
}
|
||||
}
|
||||
|
||||
async resolvePages (templateContext) {
|
||||
const { templateVars } = templateContext
|
||||
|
||||
const pagesDir = path.join(this.options.srcDir, this.options.dir.pages)
|
||||
this._nuxtPages = templateContext.hasPages = await fsExtra.exists(pagesDir)
|
||||
|
||||
if (!templateContext.hasPages) {
|
||||
return
|
||||
}
|
||||
|
||||
const { routeNameSplitter, trailingSlash } = this.options.router
|
||||
|
||||
// Use nuxt.js createRoutes bases on pages/
|
||||
const files = {}
|
||||
const ext = new RegExp(`\\.(${this.supportedExtensions.join('|')})$`)
|
||||
for (const page of await this.resolveFiles(this.options.dir.pages)) {
|
||||
const key = page.replace(ext, '')
|
||||
// .vue file takes precedence over other extensions
|
||||
if (/\.vue$/.test(page) || !files[key]) {
|
||||
files[key] = page.replace(/(['"])/g, '\\$1')
|
||||
}
|
||||
}
|
||||
|
||||
templateVars.router.routes = createRoutes({
|
||||
files: Object.values(files),
|
||||
srcDir: this.options.srcDir,
|
||||
pagesDir: this.options.dir.pages,
|
||||
routeNameSplitter,
|
||||
supportedExtensions: this.supportedExtensions,
|
||||
trailingSlash
|
||||
})
|
||||
|
||||
// TODO: Support custom createRoutes
|
||||
// else { // If user defined a custom method to create routes
|
||||
// templateVars.router.routes = await this.options.build.createRoutes(
|
||||
// this.options.srcDir
|
||||
// )
|
||||
// }
|
||||
|
||||
await this.nuxt.callHook(
|
||||
'build:extendRoutes',
|
||||
templateVars.router.routes,
|
||||
r
|
||||
)
|
||||
// router.extendRoutes method
|
||||
if (typeof this.options.router.extendRoutes === 'function') {
|
||||
// let the user extend the routes
|
||||
const extendedRoutes = this.options.router.extendRoutes(
|
||||
templateVars.router.routes,
|
||||
r
|
||||
)
|
||||
// Only overwrite routes when something is returned for backwards compatibility
|
||||
if (extendedRoutes !== undefined) {
|
||||
templateVars.router.routes = extendedRoutes
|
||||
}
|
||||
}
|
||||
|
||||
// Make routes accessible for other modules and webpack configs
|
||||
this.routes = templateVars.router.routes
|
||||
}
|
||||
|
||||
async resolveStore ({ templateVars, templateFiles }) {
|
||||
// Add store if needed
|
||||
if (!this.options.features.store || !this.options.store) {
|
||||
return
|
||||
}
|
||||
|
||||
templateVars.storeModules = (await this.resolveRelative(this.options.dir.store))
|
||||
.sort(({ src: p1 }, { src: p2 }) => {
|
||||
// modules are sorted from low to high priority (for overwriting properties)
|
||||
let res = p1.split('/').length - p2.split('/').length
|
||||
if (res === 0 && p1.includes('/index.')) {
|
||||
res = -1
|
||||
} else if (res === 0 && p2.includes('/index.')) {
|
||||
res = 1
|
||||
}
|
||||
return res
|
||||
})
|
||||
|
||||
templateFiles.push('store.js')
|
||||
}
|
||||
|
||||
async resolveMiddleware ({ templateVars, templateFiles }) {
|
||||
if (!this.options.features.middleware) {
|
||||
return
|
||||
}
|
||||
|
||||
const middleware = await this.resolveRelative(this.options.dir.middleware)
|
||||
const extRE = new RegExp(`\\.(${this.supportedExtensions.join('|')})$`)
|
||||
templateVars.middleware = middleware.map(({ src }) => {
|
||||
const name = src.replace(extRE, '')
|
||||
const dst = this.relativeToBuild(this.options.srcDir, this.options.dir.middleware, src)
|
||||
return { name, src, dst }
|
||||
})
|
||||
|
||||
// templateFiles.push('middleware.js')
|
||||
}
|
||||
|
||||
async resolveCustomTemplates (templateContext) {
|
||||
// Sanitize custom template files
|
||||
this.options.build.templates = this.options.build.templates.map((t) => {
|
||||
const src = t.src || t
|
||||
return {
|
||||
src: r(this.options.srcDir, src),
|
||||
dst: t.dst || path.basename(src),
|
||||
custom: true,
|
||||
...(typeof t === 'object' ? t : undefined)
|
||||
}
|
||||
})
|
||||
|
||||
const customTemplateFiles = this.options.build.templates.map(t => t.dst || path.basename(t.src || t))
|
||||
|
||||
const templatePaths = uniq([
|
||||
// Modules & user provided templates
|
||||
// first custom to keep their index
|
||||
...customTemplateFiles,
|
||||
// @nuxt/vue-app templates
|
||||
...templateContext.templateFiles
|
||||
])
|
||||
|
||||
const appDir = path.resolve(this.options.srcDir, this.options.dir.app)
|
||||
|
||||
templateContext.templateFiles = await Promise.all(templatePaths.map(async (file) => {
|
||||
// Use custom file if provided in build.templates[]
|
||||
const customTemplateIndex = customTemplateFiles.indexOf(file)
|
||||
const customTemplate = customTemplateIndex !== -1 ? this.options.build.templates[customTemplateIndex] : null
|
||||
let src = customTemplate ? (customTemplate.src || customTemplate) : r(this.template.dir, file)
|
||||
|
||||
// Allow override templates using a file with same name in ${srcDir}/app
|
||||
const customAppFile = path.resolve(this.options.srcDir, this.options.dir.app, file)
|
||||
const customAppFileExists = customAppFile.startsWith(appDir) && await fsExtra.exists(customAppFile)
|
||||
if (customAppFileExists) {
|
||||
src = customAppFile
|
||||
}
|
||||
|
||||
return {
|
||||
src,
|
||||
dst: file,
|
||||
custom: Boolean(customAppFileExists || customTemplate),
|
||||
options: (customTemplate && customTemplate.options) || {}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async resolveLoadingIndicator ({ templateFiles }) {
|
||||
if (!this.options.loadingIndicator.name) {
|
||||
return
|
||||
}
|
||||
let indicatorPath = path.resolve(
|
||||
this.template.dir,
|
||||
'views/loading',
|
||||
this.options.loadingIndicator.name + '.html'
|
||||
)
|
||||
|
||||
let customIndicator = false
|
||||
if (!await fsExtra.exists(indicatorPath)) {
|
||||
indicatorPath = this.nuxt.resolver.resolveAlias(
|
||||
this.options.loadingIndicator.name
|
||||
)
|
||||
|
||||
if (await fsExtra.exists(indicatorPath)) {
|
||||
customIndicator = true
|
||||
} else {
|
||||
indicatorPath = null
|
||||
}
|
||||
}
|
||||
|
||||
if (!indicatorPath) {
|
||||
// TODO
|
||||
// consola.error(
|
||||
// `Could not fetch loading indicator: ${
|
||||
// this.options.loadingIndicator.name
|
||||
// }`
|
||||
// )
|
||||
return
|
||||
}
|
||||
|
||||
templateFiles.push({
|
||||
src: indicatorPath,
|
||||
dst: 'loading.html',
|
||||
custom: customIndicator,
|
||||
options: this.options.loadingIndicator
|
||||
})
|
||||
}
|
||||
|
||||
async compileTemplates (templateContext) {
|
||||
// Prepare template options
|
||||
const { templateVars, templateFiles, templateOptions } = templateContext
|
||||
|
||||
await this.nuxt.callHook('build:templates', {
|
||||
templateVars,
|
||||
templatesFiles: templateFiles,
|
||||
resolve: r
|
||||
})
|
||||
|
||||
templateOptions.imports = {
|
||||
...templateOptions.imports,
|
||||
resolvePath: this.nuxt.resolver.resolvePath,
|
||||
resolveAlias: this.nuxt.resolver.resolveAlias,
|
||||
relativeToBuild: this.relativeToBuild
|
||||
}
|
||||
|
||||
// Interpret and move template files to .nuxt/
|
||||
await Promise.all(
|
||||
templateFiles.map(async (templateFile) => {
|
||||
const { src, dst, custom } = templateFile
|
||||
|
||||
// Add custom templates to watcher
|
||||
if (custom) {
|
||||
this.options.build.watch.push(src)
|
||||
}
|
||||
|
||||
// Render template to dst
|
||||
const fileContent = await fsExtra.readFile(src, 'utf8')
|
||||
|
||||
let content
|
||||
try {
|
||||
const templateFunction = template(fileContent, templateOptions)
|
||||
content = stripWhitespace(
|
||||
templateFunction({
|
||||
...templateVars,
|
||||
...templateFile
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
throw new Error(`Could not compile template ${src}: ${err.message}`)
|
||||
}
|
||||
|
||||
// Ensure parent dir exits and write file
|
||||
const relativePath = r(this.options.buildDir, dst)
|
||||
await fsExtra.outputFile(relativePath, content, 'utf8')
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
resolvePlugins () {
|
||||
// Check plugins exist then set alias to their real path
|
||||
return Promise.all(this.plugins.map(async (p) => {
|
||||
const ext = '{?(.+([^.])),/index.+([^.])}'
|
||||
const pluginFiles = await glob(`${p.src}${ext}`)
|
||||
|
||||
if (!pluginFiles || pluginFiles.length === 0) {
|
||||
throw new Error(`Plugin not found: ${p.src}`)
|
||||
}
|
||||
|
||||
if (pluginFiles.length > 1 && !isIndexFileAndFolder(pluginFiles)) {
|
||||
consola.warn({
|
||||
message: `Found ${pluginFiles.length} plugins that match the configuration, suggest to specify extension:`,
|
||||
additional: '\n' + pluginFiles.map(x => `- ${x}`).join('\n')
|
||||
})
|
||||
}
|
||||
|
||||
p.src = this.relativeToBuild(p.src)
|
||||
}))
|
||||
}
|
||||
|
||||
// TODO: Uncomment when generateConfig enabled again
|
||||
// async generateConfig() {
|
||||
// const config = path.resolve(this.options.buildDir, 'build.config.js')
|
||||
// const options = omit(this.options, Options.unsafeKeys)
|
||||
// await fsExtra.writeFile(
|
||||
// config,
|
||||
// `export default ${JSON.stringify(options, null, ' ')}`,
|
||||
// 'utf8'
|
||||
// )
|
||||
// }
|
||||
|
||||
createFileWatcher (patterns, events, listener, watcherCreatedCallback) {
|
||||
const options = this.options.watchers.chokidar
|
||||
const watcher = chokidar.watch(patterns, options)
|
||||
|
||||
for (const event of events) {
|
||||
watcher.on(event, listener)
|
||||
}
|
||||
|
||||
// TODO: due to fixes in chokidar this isnt used anymore and could be removed in Nuxt v3
|
||||
const { rewatchOnRawEvents } = this.options.watchers
|
||||
if (rewatchOnRawEvents && Array.isArray(rewatchOnRawEvents)) {
|
||||
watcher.on('raw', (_event) => {
|
||||
if (rewatchOnRawEvents.includes(_event)) {
|
||||
watcher.close()
|
||||
|
||||
listener()
|
||||
this.createFileWatcher(patterns, events, listener, watcherCreatedCallback)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof watcherCreatedCallback === 'function') {
|
||||
watcherCreatedCallback(watcher)
|
||||
}
|
||||
}
|
||||
|
||||
assignWatcher (key) {
|
||||
return (watcher) => {
|
||||
if (this.watchers[key]) {
|
||||
this.watchers[key].close()
|
||||
}
|
||||
this.watchers[key] = watcher
|
||||
}
|
||||
}
|
||||
|
||||
watchClient () {
|
||||
let patterns = [
|
||||
r(this.options.srcDir, this.options.dir.layouts),
|
||||
r(this.options.srcDir, this.options.dir.middleware),
|
||||
...this.appFiles
|
||||
]
|
||||
|
||||
if (this.options.store) {
|
||||
patterns.push(r(this.options.srcDir, this.options.dir.store))
|
||||
}
|
||||
|
||||
if (this._nuxtPages && !this._defaultPage) {
|
||||
patterns.push(r(this.options.srcDir, this.options.dir.pages))
|
||||
}
|
||||
|
||||
patterns = patterns.map((path, ...args) => upath.normalizeSafe(this.globPathWithExtensions(path), ...args))
|
||||
|
||||
const refreshFiles = debounce(() => this.generateRoutesAndFiles(), 200)
|
||||
|
||||
// Watch for src Files
|
||||
this.createFileWatcher(patterns, ['add', 'unlink'], refreshFiles, this.assignWatcher('files'))
|
||||
|
||||
// Watch for custom provided files
|
||||
const customPatterns = uniq([
|
||||
...this.options.build.watch,
|
||||
...Object.values(omit(this.options.build.styleResources, ['options']))
|
||||
]).map(upath.normalizeSafe)
|
||||
|
||||
if (customPatterns.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.createFileWatcher(customPatterns, ['change'], refreshFiles, this.assignWatcher('custom'))
|
||||
|
||||
// Watch for app/ files
|
||||
this.createFileWatcher([r(this.options.srcDir, this.options.dir.app)], ['add', 'change', 'unlink'], refreshFiles, this.assignWatcher('app'))
|
||||
}
|
||||
|
||||
serverMiddlewareHMR () {
|
||||
// Check nuxt.server dependency
|
||||
if (!this.nuxt.server) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get registered server middleware with path
|
||||
const entries = this.nuxt.server.serverMiddlewarePaths()
|
||||
|
||||
// Resolve dependency tree
|
||||
const deps = new Set()
|
||||
const dep2Entry = {}
|
||||
|
||||
for (const entry of entries) {
|
||||
for (const dep of scanRequireTree(entry)) {
|
||||
deps.add(dep)
|
||||
if (!dep2Entry[dep]) {
|
||||
dep2Entry[dep] = new Set()
|
||||
}
|
||||
dep2Entry[dep].add(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Create watcher
|
||||
this.createFileWatcher(
|
||||
Array.from(deps),
|
||||
['all'],
|
||||
debounce((event, fileName) => {
|
||||
if (!dep2Entry[fileName]) {
|
||||
return // #7097
|
||||
}
|
||||
for (const entry of dep2Entry[fileName]) {
|
||||
// Reload entry
|
||||
let newItem
|
||||
try {
|
||||
newItem = this.nuxt.server.replaceMiddleware(entry, entry)
|
||||
} catch (error) {
|
||||
consola.error(error)
|
||||
consola.error(`[HMR Error]: ${error}`)
|
||||
}
|
||||
|
||||
if (!newItem) {
|
||||
// Full reload if HMR failed
|
||||
return this.nuxt.callHook('watch:restart', { event, path: fileName })
|
||||
}
|
||||
|
||||
// Log
|
||||
consola.info(`[HMR] ${chalk.cyan(newItem.route || '/')} (${chalk.grey(fileName)})`)
|
||||
}
|
||||
// Tree may be changed so recreate watcher
|
||||
this.serverMiddlewareHMR()
|
||||
}, 200),
|
||||
this.assignWatcher('serverMiddleware')
|
||||
)
|
||||
}
|
||||
|
||||
watchRestart () {
|
||||
const nuxtRestartWatch = [
|
||||
// Custom watchers
|
||||
...this.options.watch
|
||||
].map(this.nuxt.resolver.resolveAlias)
|
||||
|
||||
if (this.ignore.ignoreFile) {
|
||||
nuxtRestartWatch.push(this.ignore.ignoreFile)
|
||||
}
|
||||
|
||||
if (this.options._envConfig && this.options._envConfig.dotenv) {
|
||||
nuxtRestartWatch.push(this.options._envConfig.dotenv)
|
||||
}
|
||||
|
||||
// If default page displayed, watch for first page creation
|
||||
if (this._nuxtPages && this._defaultPage) {
|
||||
nuxtRestartWatch.push(path.join(this.options.srcDir, this.options.dir.pages))
|
||||
}
|
||||
// If store not activated, watch for a file in the directory
|
||||
if (!this.options.store) {
|
||||
nuxtRestartWatch.push(path.join(this.options.srcDir, this.options.dir.store))
|
||||
}
|
||||
|
||||
this.createFileWatcher(
|
||||
nuxtRestartWatch,
|
||||
['all'],
|
||||
async (event, fileName) => {
|
||||
if (['add', 'change', 'unlink'].includes(event) === false) {
|
||||
return
|
||||
}
|
||||
await this.nuxt.callHook('watch:fileChanged', this, fileName) // Legacy
|
||||
await this.nuxt.callHook('watch:restart', { event, path: fileName })
|
||||
},
|
||||
this.assignWatcher('restart')
|
||||
)
|
||||
}
|
||||
|
||||
unwatch () {
|
||||
for (const watcher in this.watchers) {
|
||||
this.watchers[watcher].close()
|
||||
}
|
||||
}
|
||||
|
||||
async close () {
|
||||
if (this.__closed) {
|
||||
return
|
||||
}
|
||||
this.__closed = true
|
||||
|
||||
// Unwatch
|
||||
this.unwatch()
|
||||
|
||||
// Close bundleBuilder
|
||||
if (typeof this.bundleBuilder.close === 'function') {
|
||||
await this.bundleBuilder.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const STATUS = {
|
||||
INITIAL: 1,
|
||||
BUILD_DONE: 2,
|
||||
BUILDING: 3
|
||||
}
|
16
packages/nuxt3/src/builder/context/build.ts
Normal file
16
packages/nuxt3/src/builder/context/build.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export default class BuildContext {
|
||||
constructor (builder) {
|
||||
this._builder = builder
|
||||
this.nuxt = builder.nuxt
|
||||
this.options = builder.nuxt.options
|
||||
this.target = builder.nuxt.options.target
|
||||
}
|
||||
|
||||
get buildOptions () {
|
||||
return this.options.build
|
||||
}
|
||||
|
||||
get plugins () {
|
||||
return this._builder.plugins
|
||||
}
|
||||
}
|
70
packages/nuxt3/src/builder/context/template.ts
Normal file
70
packages/nuxt3/src/builder/context/template.ts
Normal file
@ -0,0 +1,70 @@
|
||||
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'
|
||||
|
||||
export default class TemplateContext {
|
||||
constructor(builder, options) {
|
||||
this.templateFiles = Array.from(builder.template.files)
|
||||
this.templateVars = {
|
||||
nuxtOptions: options,
|
||||
features: options.features,
|
||||
extensions: options.extensions
|
||||
.map(ext => ext.replace(/^\./, ''))
|
||||
.join('|'),
|
||||
messages: options.messages,
|
||||
splitChunks: options.build.splitChunks,
|
||||
uniqBy,
|
||||
isDev: options.dev,
|
||||
isTest: options.test,
|
||||
isFullStatic: isFullStatic(options),
|
||||
debug: options.debug,
|
||||
buildIndicator: options.dev && options.build.indicator,
|
||||
vue: { config: options.vue.config },
|
||||
fetch: options.fetch,
|
||||
mode: options.mode,
|
||||
router: options.router,
|
||||
env: options.env,
|
||||
head: options.head,
|
||||
store: options.features.store ? options.store : false,
|
||||
globalName: options.globalName,
|
||||
globals: builder.globals,
|
||||
css: options.css,
|
||||
plugins: builder.plugins,
|
||||
appPath: './App.js',
|
||||
layouts: Object.assign({}, options.layouts),
|
||||
loading:
|
||||
typeof options.loading === 'string'
|
||||
? builder.relativeToBuild(options.srcDir, options.loading)
|
||||
: options.loading,
|
||||
pageTransition: options.pageTransition,
|
||||
layoutTransition: options.layoutTransition,
|
||||
rootDir: options.rootDir,
|
||||
srcDir: options.srcDir,
|
||||
dir: options.dir,
|
||||
components: {
|
||||
ErrorPage: options.ErrorPage
|
||||
? builder.relativeToBuild(options.ErrorPage)
|
||||
: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get templateOptions () {
|
||||
return {
|
||||
imports: {
|
||||
serialize,
|
||||
serializeFunction,
|
||||
devalue,
|
||||
hash,
|
||||
r,
|
||||
wp,
|
||||
wChunk,
|
||||
},
|
||||
interpolate: /<%=([\s\S]+?)%>/g
|
||||
}
|
||||
}
|
||||
}
|
59
packages/nuxt3/src/builder/ignore.ts
Normal file
59
packages/nuxt3/src/builder/ignore.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import ignore from 'ignore'
|
||||
|
||||
export default class Ignore {
|
||||
constructor (options) {
|
||||
this.rootDir = options.rootDir
|
||||
this.ignoreOptions = options.ignoreOptions
|
||||
this.ignoreArray = options.ignoreArray
|
||||
this.addIgnoresRules()
|
||||
}
|
||||
|
||||
static get IGNORE_FILENAME () {
|
||||
return '.nuxtignore'
|
||||
}
|
||||
|
||||
findIgnoreFile () {
|
||||
if (!this.ignoreFile) {
|
||||
const ignoreFile = path.resolve(this.rootDir, Ignore.IGNORE_FILENAME)
|
||||
if (fs.existsSync(ignoreFile) && fs.statSync(ignoreFile).isFile()) {
|
||||
this.ignoreFile = ignoreFile
|
||||
this.ignore = ignore(this.ignoreOptions)
|
||||
}
|
||||
}
|
||||
return this.ignoreFile
|
||||
}
|
||||
|
||||
readIgnoreFile () {
|
||||
if (this.findIgnoreFile()) {
|
||||
return fs.readFileSync(this.ignoreFile, 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
addIgnoresRules () {
|
||||
const content = this.readIgnoreFile()
|
||||
if (content) {
|
||||
this.ignore.add(content)
|
||||
}
|
||||
if (this.ignoreArray && this.ignoreArray.length > 0) {
|
||||
if (!this.ignore) {
|
||||
this.ignore = ignore(this.ignoreOptions)
|
||||
}
|
||||
this.ignore.add(this.ignoreArray)
|
||||
}
|
||||
}
|
||||
|
||||
filter (paths) {
|
||||
if (this.ignore) {
|
||||
return this.ignore.filter([].concat(paths || []))
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
reload () {
|
||||
delete this.ignore
|
||||
delete this.ignoreFile
|
||||
this.addIgnoresRules()
|
||||
}
|
||||
}
|
10
packages/nuxt3/src/builder/index.ts
Normal file
10
packages/nuxt3/src/builder/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import Builder from './builder'
|
||||
export { default as Builder } from './builder'
|
||||
|
||||
export function getBuilder (nuxt) {
|
||||
return new Builder(nuxt)
|
||||
}
|
||||
|
||||
export function build (nuxt) {
|
||||
return getBuilder(nuxt).build()
|
||||
}
|
237
packages/nuxt3/src/cli/command.ts
Normal file
237
packages/nuxt3/src/cli/command.ts
Normal file
@ -0,0 +1,237 @@
|
||||
|
||||
import path from 'path'
|
||||
import consola from 'consola'
|
||||
import minimist from 'minimist'
|
||||
import Hookable from 'hable'
|
||||
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 default class NuxtCommand extends Hookable {
|
||||
constructor (cmd = { name: '', usage: '', description: '' }, argv = process.argv.slice(2), hooks = {}) {
|
||||
super(consola)
|
||||
this.addHooks(hooks)
|
||||
|
||||
if (!cmd.options) {
|
||||
cmd.options = {}
|
||||
}
|
||||
this.cmd = cmd
|
||||
|
||||
this._argv = Array.from(argv)
|
||||
this._parsedArgv = null // Lazy evaluate
|
||||
}
|
||||
|
||||
static run (cmd, argv, hooks) {
|
||||
return NuxtCommand.from(cmd, argv, hooks).run()
|
||||
}
|
||||
|
||||
static from (cmd, argv, hooks) {
|
||||
if (cmd instanceof NuxtCommand) {
|
||||
return cmd
|
||||
}
|
||||
return new NuxtCommand(cmd, argv, hooks)
|
||||
}
|
||||
|
||||
async run () {
|
||||
await this.callHook('run:before', {
|
||||
argv: this._argv,
|
||||
cmd: this.cmd,
|
||||
rootDir: path.resolve(this.argv._[0] || '.')
|
||||
})
|
||||
|
||||
if (this.argv.help) {
|
||||
this.showHelp()
|
||||
return
|
||||
}
|
||||
|
||||
if (this.argv.version) {
|
||||
this.showVersion()
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof this.cmd.run !== 'function') {
|
||||
throw new TypeError('Invalid command! Commands should at least implement run() function.')
|
||||
}
|
||||
|
||||
let cmdError
|
||||
|
||||
try {
|
||||
await this.cmd.run(this)
|
||||
} catch (e) {
|
||||
cmdError = e
|
||||
}
|
||||
|
||||
if (this.argv.lock) {
|
||||
await this.releaseLock()
|
||||
}
|
||||
|
||||
if (this.argv['force-exit']) {
|
||||
const forceExitByUser = this.isUserSuppliedArg('force-exit')
|
||||
if (cmdError) {
|
||||
consola.fatal(cmdError)
|
||||
}
|
||||
forceExit(this.cmd.name, forceExitByUser ? false : forceExitTimeout)
|
||||
if (forceExitByUser) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdError) {
|
||||
throw cmdError
|
||||
}
|
||||
}
|
||||
|
||||
showVersion () {
|
||||
process.stdout.write(`${name} v${version}\n`)
|
||||
}
|
||||
|
||||
showHelp () {
|
||||
process.stdout.write(this._getHelp())
|
||||
}
|
||||
|
||||
get argv () {
|
||||
if (!this._parsedArgv) {
|
||||
const minimistOptions = this._getMinimistOptions()
|
||||
this._parsedArgv = minimist(this._argv, minimistOptions)
|
||||
}
|
||||
return this._parsedArgv
|
||||
}
|
||||
|
||||
async getNuxtConfig (extraOptions = {}) {
|
||||
// Flag to indicate nuxt is running with CLI (not programmatic)
|
||||
extraOptions._cli = true
|
||||
|
||||
const context = {
|
||||
command: this.cmd.name,
|
||||
dev: !!extraOptions.dev
|
||||
}
|
||||
|
||||
const config = await loadNuxtConfig(this.argv, context)
|
||||
const options = Object.assign(config, extraOptions)
|
||||
|
||||
for (const name of Object.keys(this.cmd.options)) {
|
||||
this.cmd.options[name].prepare && this.cmd.options[name].prepare(this, options, this.argv)
|
||||
}
|
||||
|
||||
await this.callHook('config', options)
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
async getNuxt (options) {
|
||||
|
||||
const nuxt = new Nuxt(options)
|
||||
await nuxt.ready()
|
||||
|
||||
return nuxt
|
||||
}
|
||||
|
||||
async getBuilder (nuxt) {
|
||||
return new Builder(nuxt)
|
||||
}
|
||||
|
||||
async getGenerator (nuxt) {
|
||||
const builder = await this.getBuilder(nuxt)
|
||||
return new Generator(nuxt, builder)
|
||||
}
|
||||
|
||||
async setLock (lockRelease) {
|
||||
if (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`)
|
||||
|
||||
await this.releaseLock()
|
||||
this._lockRelease = lockRelease
|
||||
} else {
|
||||
this._lockRelease = lockRelease
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async releaseLock () {
|
||||
if (this._lockRelease) {
|
||||
await this._lockRelease()
|
||||
this._lockRelease = undefined
|
||||
}
|
||||
}
|
||||
|
||||
isUserSuppliedArg (option) {
|
||||
return this._argv.includes(`--${option}`) || this._argv.includes(`--no-${option}`)
|
||||
}
|
||||
|
||||
_getDefaultOptionValue (option) {
|
||||
return typeof option.default === 'function' ? option.default(this.cmd) : option.default
|
||||
}
|
||||
|
||||
_getMinimistOptions () {
|
||||
const minimistOptions = {
|
||||
alias: {},
|
||||
boolean: [],
|
||||
string: [],
|
||||
default: {}
|
||||
}
|
||||
|
||||
for (const name of Object.keys(this.cmd.options)) {
|
||||
const option = this.cmd.options[name]
|
||||
|
||||
if (option.alias) {
|
||||
minimistOptions.alias[option.alias] = name
|
||||
}
|
||||
if (option.type) {
|
||||
minimistOptions[option.type].push(option.alias || name)
|
||||
}
|
||||
if (option.default) {
|
||||
minimistOptions.default[option.alias || name] = this._getDefaultOptionValue(option)
|
||||
}
|
||||
}
|
||||
|
||||
return minimistOptions
|
||||
}
|
||||
|
||||
_getHelp () {
|
||||
const options = []
|
||||
let maxOptionLength = 0
|
||||
|
||||
for (const name in this.cmd.options) {
|
||||
const option = this.cmd.options[name]
|
||||
|
||||
let optionHelp = '--'
|
||||
optionHelp += option.type === 'boolean' && this._getDefaultOptionValue(option) ? 'no-' : ''
|
||||
optionHelp += name
|
||||
if (option.alias) {
|
||||
optionHelp += `, -${option.alias}`
|
||||
}
|
||||
|
||||
maxOptionLength = Math.max(maxOptionLength, optionHelp.length)
|
||||
options.push([optionHelp, option.description])
|
||||
}
|
||||
|
||||
const _opts = options.map(([option, description]) => {
|
||||
const i = indent(maxOptionLength + optionSpaces - option.length)
|
||||
return foldLines(
|
||||
option + i + description,
|
||||
startSpaces + maxOptionLength + optionSpaces * 2,
|
||||
startSpaces + optionSpaces
|
||||
)
|
||||
}).join('\n')
|
||||
|
||||
const usage = foldLines(`Usage: nuxt ${this.cmd.usage} [options]`, startSpaces)
|
||||
const description = foldLines(this.cmd.description, startSpaces)
|
||||
const opts = foldLines('Options:', startSpaces) + '\n\n' + _opts
|
||||
|
||||
let helpText = colorize(`${usage}\n\n`)
|
||||
if (this.cmd.description) {
|
||||
helpText += colorize(`${description}\n\n`)
|
||||
}
|
||||
if (options.length) {
|
||||
helpText += colorize(`${opts}\n\n`)
|
||||
}
|
||||
|
||||
return helpText
|
||||
}
|
||||
}
|
92
packages/nuxt3/src/cli/commands/build.ts
Normal file
92
packages/nuxt3/src/cli/commands/build.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import consola from 'consola'
|
||||
import { MODES, TARGETS } from 'src/utils'
|
||||
import { common, locking } from '../options'
|
||||
import { createLock } from '../utils'
|
||||
|
||||
export default {
|
||||
name: 'build',
|
||||
description: 'Compiles the application for production deployment',
|
||||
usage: 'build <dir>',
|
||||
options: {
|
||||
...common,
|
||||
...locking,
|
||||
analyze: {
|
||||
alias: 'a',
|
||||
type: 'boolean',
|
||||
description: 'Launch webpack-bundle-analyzer to optimize your bundles',
|
||||
prepare (cmd, options, argv) {
|
||||
// Analyze option
|
||||
options.build = options.build || {}
|
||||
if (argv.analyze && typeof options.build.analyze !== 'object') {
|
||||
options.build.analyze = true
|
||||
}
|
||||
}
|
||||
},
|
||||
devtools: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Enable Vue devtools',
|
||||
prepare (cmd, options, argv) {
|
||||
options.vue = options.vue || {}
|
||||
options.vue.config = options.vue.config || {}
|
||||
if (argv.devtools) {
|
||||
options.vue.config.devtools = true
|
||||
}
|
||||
}
|
||||
},
|
||||
generate: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Don\'t generate static version for SPA mode (useful for nuxt start)'
|
||||
},
|
||||
quiet: {
|
||||
alias: 'q',
|
||||
type: 'boolean',
|
||||
description: 'Disable output except for errors',
|
||||
prepare (cmd, options, argv) {
|
||||
// Silence output when using --quiet
|
||||
options.build = options.build || {}
|
||||
if (argv.quiet) {
|
||||
options.build.quiet = Boolean(argv.quiet)
|
||||
}
|
||||
}
|
||||
},
|
||||
standalone: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Bundle all server dependencies (useful for nuxt-start)',
|
||||
prepare (cmd, options, argv) {
|
||||
if (argv.standalone) {
|
||||
options.build.standalone = true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async run (cmd) {
|
||||
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)
|
||||
|
||||
if (cmd.argv.lock) {
|
||||
await cmd.setLock(await createLock({
|
||||
id: 'build',
|
||||
dir: nuxt.options.buildDir,
|
||||
root: config.rootDir
|
||||
}))
|
||||
}
|
||||
|
||||
// TODO: remove if in Nuxt 3
|
||||
if (nuxt.options.mode === MODES.spa && nuxt.options.target === TARGETS.server && cmd.argv.generate !== false) {
|
||||
// Build + Generate for static deployment
|
||||
const generator = await cmd.getGenerator(nuxt)
|
||||
await generator.generate({ build: true })
|
||||
} else {
|
||||
// Build only
|
||||
const builder = await cmd.getBuilder(nuxt)
|
||||
await builder.build()
|
||||
|
||||
const nextCommand = nuxt.options.target === TARGETS.static ? 'nuxt export' : 'nuxt start'
|
||||
consola.info('Ready to run `' + (nextCommand) + '`')
|
||||
}
|
||||
}
|
||||
}
|
116
packages/nuxt3/src/cli/commands/dev.ts
Normal file
116
packages/nuxt3/src/cli/commands/dev.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import consola from 'consola'
|
||||
import chalk from 'chalk'
|
||||
import opener from 'opener'
|
||||
import { common, server } from '../options'
|
||||
import { eventsMapping, formatPath } from '../utils'
|
||||
import { showBanner } from '../utils/banner'
|
||||
import { showMemoryUsage } from '../utils/memory'
|
||||
|
||||
export default {
|
||||
name: 'dev',
|
||||
description: 'Start the application in development mode (e.g. hot-code reloading, error reporting)',
|
||||
usage: 'dev <dir>',
|
||||
options: {
|
||||
...common,
|
||||
...server,
|
||||
open: {
|
||||
alias: 'o',
|
||||
type: 'boolean',
|
||||
description: 'Opens the server listeners url in the default browser'
|
||||
}
|
||||
},
|
||||
|
||||
async run (cmd) {
|
||||
const { argv } = cmd
|
||||
|
||||
await this.startDev(cmd, argv, argv.open)
|
||||
},
|
||||
|
||||
async startDev (cmd, argv) {
|
||||
let nuxt
|
||||
try {
|
||||
nuxt = await this._listenDev(cmd, argv)
|
||||
} catch (error) {
|
||||
consola.fatal(error)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this._buildDev(cmd, argv, nuxt)
|
||||
} catch (error) {
|
||||
await nuxt.callHook('cli:buildError', error)
|
||||
consola.error(error)
|
||||
}
|
||||
|
||||
return nuxt
|
||||
},
|
||||
|
||||
async _listenDev (cmd, argv) {
|
||||
const config = await cmd.getNuxtConfig({ dev: true, _build: true })
|
||||
const nuxt = await cmd.getNuxt(config)
|
||||
|
||||
// Setup hooks
|
||||
nuxt.hook('watch:restart', payload => this.onWatchRestart(payload, { nuxt, cmd, argv }))
|
||||
nuxt.hook('bundler:change', changedFileName => this.onBundlerChange(changedFileName))
|
||||
|
||||
// Wait for nuxt to be ready
|
||||
await nuxt.ready()
|
||||
|
||||
// Start listening
|
||||
await nuxt.server.listen()
|
||||
|
||||
// Show banner when listening
|
||||
showBanner(nuxt, false)
|
||||
|
||||
// Opens the server listeners url in the default browser (only once)
|
||||
if (argv.open) {
|
||||
argv.open = false
|
||||
const openerPromises = nuxt.server.listeners.map(listener => opener(listener.url))
|
||||
await Promise.all(openerPromises)
|
||||
}
|
||||
|
||||
// Return instance
|
||||
return nuxt
|
||||
},
|
||||
|
||||
async _buildDev (cmd, argv, nuxt) {
|
||||
// Create builder instance
|
||||
const builder = await cmd.getBuilder(nuxt)
|
||||
|
||||
// Start Build
|
||||
await builder.build()
|
||||
|
||||
// Print memory usage
|
||||
showMemoryUsage()
|
||||
|
||||
// Display server urls after the build
|
||||
for (const listener of nuxt.server.listeners) {
|
||||
consola.info(chalk.bold('Listening on: ') + listener.url)
|
||||
}
|
||||
|
||||
// Return instance
|
||||
return nuxt
|
||||
},
|
||||
|
||||
logChanged ({ event, path }) {
|
||||
const { icon, color, action } = eventsMapping[event] || eventsMapping.change
|
||||
|
||||
consola.log({
|
||||
type: event,
|
||||
icon: chalk[color].bold(icon),
|
||||
message: `${action} ${chalk.cyan(formatPath(path))}`
|
||||
})
|
||||
},
|
||||
|
||||
async onWatchRestart ({ event, path }, { nuxt, cmd, argv }) {
|
||||
this.logChanged({ event, path })
|
||||
|
||||
await nuxt.close()
|
||||
|
||||
await this.startDev(cmd, argv)
|
||||
},
|
||||
|
||||
onBundlerChange (path) {
|
||||
this.logChanged({ event: 'change', path })
|
||||
}
|
||||
}
|
50
packages/nuxt3/src/cli/commands/export.ts
Normal file
50
packages/nuxt3/src/cli/commands/export.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import path from 'path'
|
||||
import consola from 'consola'
|
||||
import { TARGETS } from 'src/utils'
|
||||
import { common, locking } from '../options'
|
||||
import { createLock } from '../utils'
|
||||
|
||||
export default {
|
||||
name: 'export',
|
||||
description: 'Export a static generated web application',
|
||||
usage: 'export <dir>',
|
||||
options: {
|
||||
...common,
|
||||
...locking,
|
||||
'fail-on-error': {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Exit with non-zero status code if there are errors when exporting pages'
|
||||
}
|
||||
},
|
||||
async run (cmd) {
|
||||
const config = await cmd.getNuxtConfig({
|
||||
dev: false,
|
||||
target: TARGETS.static,
|
||||
_export: true
|
||||
})
|
||||
const nuxt = await cmd.getNuxt(config)
|
||||
|
||||
if (cmd.argv.lock) {
|
||||
await cmd.setLock(await createLock({
|
||||
id: 'export',
|
||||
dir: nuxt.options.generate.dir,
|
||||
root: config.rootDir
|
||||
}))
|
||||
}
|
||||
|
||||
const generator = await cmd.getGenerator(nuxt)
|
||||
await nuxt.server.listen(0)
|
||||
|
||||
const { errors } = await generator.generate({
|
||||
init: true,
|
||||
build: false
|
||||
})
|
||||
|
||||
await nuxt.close()
|
||||
if (cmd.argv['fail-on-error'] && errors.length > 0) {
|
||||
throw new Error('Error exporting pages, exiting with non-zero code')
|
||||
}
|
||||
consola.info('Ready to run `nuxt serve` or deploy `' + path.basename(nuxt.options.generate.dir) + '/` directory')
|
||||
}
|
||||
}
|
110
packages/nuxt3/src/cli/commands/generate.ts
Normal file
110
packages/nuxt3/src/cli/commands/generate.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { TARGETS } from 'src/utils'
|
||||
import { common, locking } from '../options'
|
||||
import { normalizeArg, createLock } from '../utils'
|
||||
|
||||
export default {
|
||||
name: 'generate',
|
||||
description: 'Generate a static web application (server-rendered)',
|
||||
usage: 'generate <dir>',
|
||||
options: {
|
||||
...common,
|
||||
...locking,
|
||||
build: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Only generate pages for dynamic routes, used for incremental builds. Generate has to be run once without this option before using it'
|
||||
},
|
||||
devtools: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Enable Vue devtools',
|
||||
prepare (cmd, options, argv) {
|
||||
options.vue = options.vue || {}
|
||||
options.vue.config = options.vue.config || {}
|
||||
if (argv.devtools) {
|
||||
options.vue.config.devtools = true
|
||||
}
|
||||
}
|
||||
},
|
||||
quiet: {
|
||||
alias: 'q',
|
||||
type: 'boolean',
|
||||
description: 'Disable output except for errors',
|
||||
prepare (cmd, options, argv) {
|
||||
// Silence output when using --quiet
|
||||
options.build = options.build || {}
|
||||
if (argv.quiet) {
|
||||
options.build.quiet = true
|
||||
}
|
||||
}
|
||||
},
|
||||
modern: {
|
||||
...common.modern,
|
||||
description: 'Generate app in modern build (modern mode can be only client)',
|
||||
prepare (cmd, options, argv) {
|
||||
if (normalizeArg(argv.modern)) {
|
||||
options.modern = 'client'
|
||||
}
|
||||
}
|
||||
},
|
||||
'fail-on-error': {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Exit with non-zero status code if there are errors when generating pages'
|
||||
}
|
||||
},
|
||||
async run (cmd) {
|
||||
const config = await cmd.getNuxtConfig({
|
||||
dev: false,
|
||||
_build: cmd.argv.build,
|
||||
_generate: true
|
||||
})
|
||||
|
||||
if (config.target === TARGETS.static) {
|
||||
throw new Error("Please use `nuxt export` when using `target: 'static'`")
|
||||
}
|
||||
|
||||
// Forcing static target anyway
|
||||
config.target = TARGETS.static
|
||||
|
||||
// Disable analyze if set by the nuxt config
|
||||
config.build = config.build || {}
|
||||
config.build.analyze = false
|
||||
|
||||
// Set flag to keep the prerendering behaviour
|
||||
config._legacyGenerate = true
|
||||
|
||||
const nuxt = await cmd.getNuxt(config)
|
||||
|
||||
if (cmd.argv.lock) {
|
||||
await cmd.setLock(await createLock({
|
||||
id: 'build',
|
||||
dir: nuxt.options.buildDir,
|
||||
root: config.rootDir
|
||||
}))
|
||||
|
||||
nuxt.hook('build:done', async () => {
|
||||
await cmd.releaseLock()
|
||||
|
||||
await cmd.setLock(await createLock({
|
||||
id: 'generate',
|
||||
dir: nuxt.options.generate.dir,
|
||||
root: config.rootDir
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
const generator = await cmd.getGenerator(nuxt)
|
||||
await nuxt.server.listen(0)
|
||||
|
||||
const { errors } = await generator.generate({
|
||||
init: true,
|
||||
build: cmd.argv.build
|
||||
})
|
||||
|
||||
await nuxt.close()
|
||||
if (cmd.argv['fail-on-error'] && errors.length > 0) {
|
||||
throw new Error('Error generating pages, exiting with non-zero code')
|
||||
}
|
||||
}
|
||||
}
|
29
packages/nuxt3/src/cli/commands/help.ts
Normal file
29
packages/nuxt3/src/cli/commands/help.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import consola from 'consola'
|
||||
import listCommands from '../list'
|
||||
import { common } from '../options'
|
||||
import NuxtCommand from '../command'
|
||||
import getCommand from '.'
|
||||
|
||||
export default {
|
||||
name: 'help',
|
||||
description: 'Shows help for <command>',
|
||||
usage: 'help <command>',
|
||||
options: {
|
||||
help: common.help,
|
||||
version: common.version
|
||||
},
|
||||
async run (cmd) {
|
||||
const [name] = cmd._argv
|
||||
if (!name) {
|
||||
return listCommands()
|
||||
}
|
||||
const command = await getCommand(name)
|
||||
|
||||
if (!command) {
|
||||
consola.info(`Unknown command: ${name}`)
|
||||
return
|
||||
}
|
||||
|
||||
NuxtCommand.from(command).showHelp()
|
||||
}
|
||||
}
|
17
packages/nuxt3/src/cli/commands/index.ts
Normal file
17
packages/nuxt3/src/cli/commands/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
const _commands = {
|
||||
start: () => import('./start'),
|
||||
serve: () => import('./serve'),
|
||||
dev: () => import('./dev'),
|
||||
build: () => import('./build'),
|
||||
generate: () => import('./generate'),
|
||||
export: () => import('./export'),
|
||||
webpack: () => import('./webpack'),
|
||||
help: () => import('./help')
|
||||
}
|
||||
|
||||
export default function getCommand (name) {
|
||||
if (!_commands[name]) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
return _commands[name]().then(m => m.default)
|
||||
}
|
83
packages/nuxt3/src/cli/commands/serve.ts
Normal file
83
packages/nuxt3/src/cli/commands/serve.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import { join, extname, basename } from 'path'
|
||||
import connect from 'connect'
|
||||
import serveStatic from 'serve-static'
|
||||
import compression from 'compression'
|
||||
import { getNuxtConfig } from 'src/config'
|
||||
import { TARGETS } from 'src/utils'
|
||||
import { common, server } from '../options'
|
||||
import { showBanner } from '../utils/banner'
|
||||
import { Listener } from 'src/server'
|
||||
import { Nuxt } from 'src/core'
|
||||
|
||||
export default {
|
||||
name: 'serve',
|
||||
description: 'Serve the exported static application (should be compiled with `nuxt build` and `nuxt export` first)',
|
||||
usage: 'serve <dir>',
|
||||
options: {
|
||||
'config-file': common['config-file'],
|
||||
version: common.version,
|
||||
help: common.help,
|
||||
...server
|
||||
},
|
||||
async run (cmd) {
|
||||
let options = await cmd.getNuxtConfig({ dev: false })
|
||||
// add default options
|
||||
options = getNuxtConfig(options)
|
||||
try {
|
||||
// overwrites with build config
|
||||
const buildConfig = require(join(options.buildDir, 'nuxt/config.json'))
|
||||
options.target = buildConfig.target
|
||||
} catch (err) {}
|
||||
|
||||
if (options.target === TARGETS.server) {
|
||||
throw new Error('You cannot use `nuxt serve` with ' + TARGETS.server + ' target, please use `nuxt start`')
|
||||
}
|
||||
const distStat = await fs.stat(options.generate.dir).catch(err => null) // eslint-disable-line handle-callback-err
|
||||
if (!distStat || !distStat.isDirectory()) {
|
||||
throw new Error('Output directory `' + basename(options.generate.dir) + '/` does not exists, please run `nuxt export` before `nuxt serve`.')
|
||||
}
|
||||
const app = connect()
|
||||
app.use(compression({ threshold: 0 }))
|
||||
app.use(
|
||||
options.router.base,
|
||||
serveStatic(options.generate.dir, {
|
||||
extensions: ['html']
|
||||
})
|
||||
)
|
||||
if (options.generate.fallback) {
|
||||
const fallbackFile = await fs.readFile(join(options.generate.dir, options.generate.fallback), 'utf-8')
|
||||
app.use((req, res, next) => {
|
||||
const ext = extname(req.url) || '.html'
|
||||
|
||||
if (ext !== '.html') {
|
||||
return next()
|
||||
}
|
||||
res.writeHeader(200, {
|
||||
'Content-Type': 'text/html'
|
||||
})
|
||||
res.write(fallbackFile)
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
|
||||
const { port, host, socket, https } = options.server
|
||||
const listener = new Listener({
|
||||
port,
|
||||
host,
|
||||
socket,
|
||||
https,
|
||||
app,
|
||||
dev: true, // try another port if taken
|
||||
baseURL: options.router.base
|
||||
})
|
||||
await listener.listen()
|
||||
showBanner({
|
||||
constructor: Nuxt,
|
||||
options,
|
||||
server: {
|
||||
listeners: [listener]
|
||||
}
|
||||
}, false)
|
||||
}
|
||||
}
|
24
packages/nuxt3/src/cli/commands/start.ts
Normal file
24
packages/nuxt3/src/cli/commands/start.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { TARGETS } from 'src/utils'
|
||||
import { common, server } from '../options'
|
||||
import { showBanner } from '../utils/banner'
|
||||
|
||||
export default {
|
||||
name: 'start',
|
||||
description: 'Start the application in production mode (the application should be compiled with `nuxt build` first)',
|
||||
usage: 'start <dir>',
|
||||
options: {
|
||||
...common,
|
||||
...server
|
||||
},
|
||||
async run (cmd) {
|
||||
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`')
|
||||
}
|
||||
const nuxt = await cmd.getNuxt(config)
|
||||
|
||||
// Listen and show ready banner
|
||||
await nuxt.server.listen()
|
||||
showBanner(nuxt)
|
||||
}
|
||||
}
|
114
packages/nuxt3/src/cli/commands/webpack.ts
Normal file
114
packages/nuxt3/src/cli/commands/webpack.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import util from 'util'
|
||||
import consola from 'consola'
|
||||
import get from 'lodash/get'
|
||||
import { common } from '../options'
|
||||
|
||||
export default {
|
||||
name: 'webpack',
|
||||
description: 'Inspect Nuxt webpack config',
|
||||
usage: 'webpack [query...]',
|
||||
options: {
|
||||
...common,
|
||||
name: {
|
||||
alias: 'n',
|
||||
type: 'string',
|
||||
default: 'client',
|
||||
description: 'Webpack bundle name: server, client, modern'
|
||||
},
|
||||
depth: {
|
||||
alias: 'd',
|
||||
type: 'string',
|
||||
default: 2,
|
||||
description: 'Inspection depth'
|
||||
},
|
||||
colors: {
|
||||
type: 'boolean',
|
||||
default: process.stdout.isTTY,
|
||||
description: 'Output with ANSI colors'
|
||||
},
|
||||
dev: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Inspect development mode webpack config'
|
||||
}
|
||||
},
|
||||
async run (cmd) {
|
||||
const { name } = cmd.argv
|
||||
const queries = [...cmd.argv._]
|
||||
|
||||
const config = await cmd.getNuxtConfig({ dev: cmd.argv.dev, server: false })
|
||||
const nuxt = await cmd.getNuxt(config)
|
||||
const builder = await cmd.getBuilder(nuxt)
|
||||
const { bundleBuilder } = builder
|
||||
const webpackConfig = bundleBuilder.getWebpackConfig(name)
|
||||
|
||||
let queryError
|
||||
const match = queries.reduce((result, query) => {
|
||||
const m = advancedGet(result, query)
|
||||
if (m === undefined) {
|
||||
queryError = query
|
||||
return result
|
||||
}
|
||||
return m
|
||||
}, webpackConfig)
|
||||
|
||||
const serialized = formatObj(match, {
|
||||
depth: parseInt(cmd.argv.depth),
|
||||
colors: cmd.argv.colors
|
||||
})
|
||||
|
||||
consola.log(serialized + '\n')
|
||||
|
||||
if (serialized.includes('[Object]' || serialized.includes('[Array'))) {
|
||||
consola.info('You can use `--depth` or add more queries to inspect `[Object]` and `[Array]` fields.')
|
||||
}
|
||||
|
||||
if (queryError) {
|
||||
consola.warn(`No match in webpack config for \`${queryError}\``)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function advancedGet (obj = {}, query = '') {
|
||||
let result = obj
|
||||
|
||||
if (!query || !result) {
|
||||
return result
|
||||
}
|
||||
|
||||
const [l, r] = query.split('=')
|
||||
|
||||
if (!Array.isArray(result)) {
|
||||
return typeof result === 'object' ? get(result, l) : result
|
||||
}
|
||||
|
||||
result = result.filter((i) => {
|
||||
const v = get(i, l)
|
||||
|
||||
if (!v) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
(v === r) ||
|
||||
(typeof v.test === 'function' && v.test(r)) ||
|
||||
(typeof v.match === 'function' && v.match(r)) ||
|
||||
(r && r.match(v))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
if (result.length === 1) {
|
||||
return result[0]
|
||||
}
|
||||
|
||||
return result.length ? result : undefined
|
||||
}
|
||||
|
||||
function formatObj (obj, formatOptions) {
|
||||
if (!util.formatWithOptions) {
|
||||
return util.format(obj)
|
||||
}
|
||||
return util.formatWithOptions(formatOptions, obj)
|
||||
}
|
13
packages/nuxt3/src/cli/index.ts
Normal file
13
packages/nuxt3/src/cli/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as commands from './commands'
|
||||
import * as options from './options'
|
||||
|
||||
export {
|
||||
commands,
|
||||
options
|
||||
}
|
||||
|
||||
export { default as NuxtCommand } from './command'
|
||||
export { default as setup } from './setup'
|
||||
export { default as run } from './run'
|
||||
export { loadNuxtConfig } from './utils/config'
|
||||
export { getWebpackConfig } from './utils/webpack'
|
35
packages/nuxt3/src/cli/list.ts
Normal file
35
packages/nuxt3/src/cli/list.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import chalk from 'chalk'
|
||||
import { indent, foldLines, colorize } from './utils/formatting'
|
||||
import { startSpaces, optionSpaces } from './utils/constants'
|
||||
import getCommand from './commands'
|
||||
|
||||
export default async function listCommands () {
|
||||
const commandsOrder = ['dev', 'build', 'generate', 'start', 'help']
|
||||
|
||||
// Load all commands
|
||||
const _commands = await Promise.all(
|
||||
commandsOrder.map(cmd => getCommand(cmd))
|
||||
)
|
||||
|
||||
let maxLength = 0
|
||||
const commandsHelp = []
|
||||
|
||||
for (const command of _commands) {
|
||||
commandsHelp.push([command.usage, command.description])
|
||||
maxLength = Math.max(maxLength, command.usage.length)
|
||||
}
|
||||
|
||||
const _cmds = commandsHelp.map(([cmd, description]) => {
|
||||
const i = indent(maxLength + optionSpaces - cmd.length)
|
||||
return foldLines(
|
||||
chalk.green(cmd) + i + description,
|
||||
startSpaces + maxLength + optionSpaces * 2,
|
||||
startSpaces + optionSpaces
|
||||
)
|
||||
}).join('\n')
|
||||
|
||||
const usage = foldLines('Usage: nuxt <command> [--help|-h]', startSpaces)
|
||||
const cmds = foldLines('Commands:', startSpaces) + '\n\n' + _cmds
|
||||
|
||||
process.stderr.write(colorize(`${usage}\n\n${cmds}\n\n`))
|
||||
}
|
68
packages/nuxt3/src/cli/options/common.ts
Normal file
68
packages/nuxt3/src/cli/options/common.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { defaultNuxtConfigFile } from 'src/config'
|
||||
import { normalizeArg } from '../utils'
|
||||
|
||||
export default {
|
||||
spa: {
|
||||
alias: 's',
|
||||
type: 'boolean',
|
||||
description: 'Launch in SPA mode'
|
||||
},
|
||||
universal: {
|
||||
alias: 'u',
|
||||
type: 'boolean',
|
||||
description: 'Launch in Universal mode (default)'
|
||||
},
|
||||
'config-file': {
|
||||
alias: 'c',
|
||||
type: 'string',
|
||||
default: defaultNuxtConfigFile,
|
||||
description: `Path to Nuxt.js config file (default: \`${defaultNuxtConfigFile}\`)`
|
||||
},
|
||||
modern: {
|
||||
alias: 'm',
|
||||
type: 'string',
|
||||
description: 'Build/Start app for modern browsers, e.g. server, client and false',
|
||||
prepare (cmd, options, argv) {
|
||||
if (argv.modern !== undefined) {
|
||||
options.modern = normalizeArg(argv.modern)
|
||||
}
|
||||
}
|
||||
},
|
||||
target: {
|
||||
alias: 't',
|
||||
type: 'string',
|
||||
description: 'Build/start app for a different target, e.g. server, serverless and static',
|
||||
prepare (cmd, options, argv) {
|
||||
if (argv.target) {
|
||||
options.target = argv.target
|
||||
}
|
||||
}
|
||||
},
|
||||
'force-exit': {
|
||||
type: 'boolean',
|
||||
default (cmd) {
|
||||
return ['build', 'generate', 'export'].includes(cmd.name)
|
||||
},
|
||||
description: 'Whether Nuxt.js should force exit after the command has finished'
|
||||
},
|
||||
version: {
|
||||
alias: 'v',
|
||||
type: 'boolean',
|
||||
description: 'Display the Nuxt version'
|
||||
},
|
||||
help: {
|
||||
alias: 'h',
|
||||
type: 'boolean',
|
||||
description: 'Display this message'
|
||||
},
|
||||
processenv: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Disable reading from `process.env` and updating it with dotenv'
|
||||
},
|
||||
dotenv: {
|
||||
type: 'string',
|
||||
default: '.env',
|
||||
description: 'Specify path to dotenv file (default: `.env`). Use `false` to disable'
|
||||
}
|
||||
}
|
9
packages/nuxt3/src/cli/options/index.ts
Normal file
9
packages/nuxt3/src/cli/options/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import common from './common'
|
||||
import server from './server'
|
||||
import locking from './locking'
|
||||
|
||||
export {
|
||||
common,
|
||||
server,
|
||||
locking
|
||||
}
|
7
packages/nuxt3/src/cli/options/locking.ts
Normal file
7
packages/nuxt3/src/cli/options/locking.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default {
|
||||
lock: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Do not set a lock on the project when building'
|
||||
}
|
||||
}
|
29
packages/nuxt3/src/cli/options/server.ts
Normal file
29
packages/nuxt3/src/cli/options/server.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import consola from 'consola'
|
||||
|
||||
export default {
|
||||
port: {
|
||||
alias: 'p',
|
||||
type: 'string',
|
||||
description: 'Port number on which to start the application',
|
||||
prepare (cmd, options, argv) {
|
||||
if (argv.port) {
|
||||
options.server.port = +argv.port
|
||||
}
|
||||
}
|
||||
},
|
||||
hostname: {
|
||||
alias: 'H',
|
||||
type: 'string',
|
||||
description: 'Hostname on which to start the application',
|
||||
prepare (cmd, options, argv) {
|
||||
if (argv.hostname === '') {
|
||||
consola.fatal('Provided hostname argument has no value')
|
||||
}
|
||||
}
|
||||
},
|
||||
'unix-socket': {
|
||||
alias: 'n',
|
||||
type: 'string',
|
||||
description: 'Path to a UNIX socket'
|
||||
}
|
||||
}
|
60
packages/nuxt3/src/cli/run.ts
Normal file
60
packages/nuxt3/src/cli/run.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import fs from 'fs'
|
||||
import execa from 'execa'
|
||||
import { name as pkgName } from '../../package.json'
|
||||
import NuxtCommand from './command'
|
||||
import setup from './setup'
|
||||
import getCommand from './commands'
|
||||
|
||||
function packageExists (name) {
|
||||
try {
|
||||
require.resolve(name)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export default async function run(_argv, hooks = {}) {
|
||||
// Check for not installing both nuxt and nuxt-edge
|
||||
const dupPkg = '@nuxt/' + (pkgName === '@nuxt/cli-edge' ? 'cli' : 'cli-edge')
|
||||
if (packageExists(dupPkg)) {
|
||||
throw new Error('Both `nuxt` and `nuxt-edge` dependencies are installed! This is unsupported, please choose one and remove the other one from dependencies.')
|
||||
}
|
||||
|
||||
// Read from process.argv
|
||||
const argv = _argv ? Array.from(_argv) : process.argv.slice(2)
|
||||
|
||||
// Check for internal command
|
||||
let cmd = await getCommand(argv[0])
|
||||
|
||||
// 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])))) {
|
||||
argv.unshift('dev')
|
||||
cmd = await getCommand('dev')
|
||||
}
|
||||
|
||||
// Check for dev
|
||||
const dev = argv[0] === 'dev'
|
||||
|
||||
// Setup env
|
||||
setup({ dev })
|
||||
|
||||
// Try internal command
|
||||
if (cmd) {
|
||||
return NuxtCommand.run(cmd, argv.slice(1), hooks)
|
||||
}
|
||||
|
||||
// Try external command
|
||||
try {
|
||||
await execa(`nuxt-${argv[0]}`, argv.slice(1), {
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
stdin: process.stdin
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.exitCode === 2) {
|
||||
throw String(`Command not found: nuxt-${argv[0]}`)
|
||||
}
|
||||
throw String(`Failed to run command \`nuxt-${argv[0]}\`:\n${error}`)
|
||||
}
|
||||
}
|
38
packages/nuxt3/src/cli/setup.ts
Normal file
38
packages/nuxt3/src/cli/setup.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import consola from 'consola'
|
||||
import exit from 'exit'
|
||||
import { fatalBox } from './utils/formatting'
|
||||
|
||||
let _setup = false
|
||||
|
||||
export default function setup ({ dev }) {
|
||||
// Apply default NODE_ENV if not provided
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = dev ? 'development' : 'production'
|
||||
}
|
||||
|
||||
if (_setup) {
|
||||
return
|
||||
}
|
||||
_setup = true
|
||||
|
||||
// Global error handler
|
||||
/* istanbul ignore next */
|
||||
process.on('unhandledRejection', (err) => {
|
||||
consola.error(err)
|
||||
})
|
||||
|
||||
// Exit process on fatal errors
|
||||
/* istanbul ignore next */
|
||||
consola.addReporter({
|
||||
log (logObj) {
|
||||
if (logObj.type === 'fatal') {
|
||||
const errorMessage = String(logObj.args[0])
|
||||
process.stderr.write(fatalBox(errorMessage))
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Wrap all console logs with consola for better DX
|
||||
consola.wrapConsole()
|
||||
}
|
60
packages/nuxt3/src/cli/utils/banner.ts
Normal file
60
packages/nuxt3/src/cli/utils/banner.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import consola from 'consola'
|
||||
import env from 'std-env'
|
||||
import chalk from 'chalk'
|
||||
import { successBox } from './formatting'
|
||||
import { getFormattedMemoryUsage } from './memory'
|
||||
|
||||
export function showBanner (nuxt, showMemoryUsage = true) {
|
||||
if (env.test) {
|
||||
return
|
||||
}
|
||||
|
||||
if (env.minimalCLI) {
|
||||
for (const listener of nuxt.server.listeners) {
|
||||
consola.info('Listening on: ' + listener.url)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const titleLines = []
|
||||
const messageLines = []
|
||||
|
||||
// Name and version
|
||||
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}:`)
|
||||
|
||||
// Environment
|
||||
const isDev = nuxt.options.dev
|
||||
let _env = isDev ? 'development' : 'production'
|
||||
if (process.env.NODE_ENV !== _env) {
|
||||
_env += ` (${chalk.cyan(process.env.NODE_ENV)})`
|
||||
}
|
||||
titleLines.push(`${label('Environment')} ${_env}`)
|
||||
|
||||
// Rendering
|
||||
const isSSR = nuxt.options.render.ssr
|
||||
const rendering = isSSR ? 'server-side' : 'client-side'
|
||||
titleLines.push(`${label('Rendering')} ${rendering}`)
|
||||
|
||||
// Target
|
||||
const target = nuxt.options.target || 'server'
|
||||
titleLines.push(`${label('Target')} ${target}`)
|
||||
|
||||
if (showMemoryUsage) {
|
||||
titleLines.push('\n' + getFormattedMemoryUsage())
|
||||
}
|
||||
|
||||
// Listeners
|
||||
for (const listener of nuxt.server.listeners) {
|
||||
messageLines.push(chalk.bold('Listening: ') + chalk.underline.blue(listener.url))
|
||||
}
|
||||
|
||||
// Add custom badge messages
|
||||
if (badgeMessages.length) {
|
||||
messageLines.push('', ...badgeMessages)
|
||||
}
|
||||
|
||||
process.stdout.write(successBox(messageLines.join('\n'), titleLines.join('\n')))
|
||||
}
|
33
packages/nuxt3/src/cli/utils/config.ts
Normal file
33
packages/nuxt3/src/cli/utils/config.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import path from 'path'
|
||||
import defaultsDeep from 'lodash/defaultsDeep'
|
||||
import { loadNuxtConfig as _loadNuxtConfig, getDefaultNuxtConfig } from 'src/config'
|
||||
import { MODES } from 'src/utils'
|
||||
|
||||
export async function loadNuxtConfig (argv, configContext) {
|
||||
const rootDir = path.resolve(argv._[0] || '.')
|
||||
const configFile = argv['config-file']
|
||||
|
||||
// Load config
|
||||
const options = await _loadNuxtConfig({
|
||||
rootDir,
|
||||
configFile,
|
||||
configContext,
|
||||
envConfig: {
|
||||
dotenv: argv.dotenv === 'false' ? false : argv.dotenv,
|
||||
env: argv.processenv ? process.env : {}
|
||||
}
|
||||
})
|
||||
|
||||
// Nuxt Mode
|
||||
options.mode =
|
||||
(argv.spa && MODES.spa) || (argv.universal && MODES.universal) || options.mode
|
||||
|
||||
// Server options
|
||||
options.server = defaultsDeep({
|
||||
port: argv.port || undefined,
|
||||
host: argv.hostname || undefined,
|
||||
socket: argv['unix-socket'] || undefined
|
||||
}, options.server || {}, getDefaultNuxtConfig().server)
|
||||
|
||||
return options
|
||||
}
|
8
packages/nuxt3/src/cli/utils/constants.ts
Normal file
8
packages/nuxt3/src/cli/utils/constants.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const forceExitTimeout = 5
|
||||
|
||||
export const startSpaces = 2
|
||||
export const optionSpaces = 2
|
||||
|
||||
// 80% of terminal column width
|
||||
// this is a fn because console width can have changed since startup
|
||||
export const maxCharsPerLine = () => (process.stdout.columns || 100) * 80 / 100
|
69
packages/nuxt3/src/cli/utils/formatting.ts
Normal file
69
packages/nuxt3/src/cli/utils/formatting.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import wrapAnsi from 'wrap-ansi'
|
||||
import chalk from 'chalk'
|
||||
import boxen from 'boxen'
|
||||
import { maxCharsPerLine } from './constants'
|
||||
|
||||
export function indent (count, chr = ' ') {
|
||||
return chr.repeat(count)
|
||||
}
|
||||
|
||||
export function indentLines (string, spaces, firstLineSpaces) {
|
||||
const lines = Array.isArray(string) ? string : string.split('\n')
|
||||
let s = ''
|
||||
if (lines.length) {
|
||||
const i0 = indent(firstLineSpaces === undefined ? spaces : firstLineSpaces)
|
||||
s = i0 + lines.shift()
|
||||
}
|
||||
if (lines.length) {
|
||||
const i = indent(spaces)
|
||||
s += '\n' + lines.map(l => i + l).join('\n')
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
export function foldLines (string, spaces, firstLineSpaces, charsPerLine = maxCharsPerLine()) {
|
||||
return indentLines(wrapAnsi(string, charsPerLine), spaces, firstLineSpaces)
|
||||
}
|
||||
|
||||
export function colorize (text) {
|
||||
return text
|
||||
.replace(/\[[^ ]+]/g, m => chalk.grey(m))
|
||||
.replace(/<[^ ]+>/g, m => chalk.green(m))
|
||||
.replace(/ (-[-\w,]+)/g, m => chalk.bold(m))
|
||||
.replace(/`([^`]+)`/g, (_, m) => chalk.bold.cyan(m))
|
||||
}
|
||||
|
||||
export function box (message, title, options) {
|
||||
return boxen([
|
||||
title || chalk.white('Nuxt Message'),
|
||||
'',
|
||||
chalk.white(foldLines(message, 0, 0, maxCharsPerLine()))
|
||||
].join('\n'), Object.assign({
|
||||
borderColor: 'white',
|
||||
borderStyle: 'round',
|
||||
padding: 1,
|
||||
margin: 1
|
||||
}, options)) + '\n'
|
||||
}
|
||||
|
||||
export function successBox (message, title) {
|
||||
return box(message, title || chalk.green('✔ Nuxt Success'), {
|
||||
borderColor: 'green'
|
||||
})
|
||||
}
|
||||
|
||||
export function warningBox (message, title) {
|
||||
return box(message, title || chalk.yellow('⚠ Nuxt Warning'), {
|
||||
borderColor: 'yellow'
|
||||
})
|
||||
}
|
||||
|
||||
export function errorBox (message, title) {
|
||||
return box(message, title || chalk.red('✖ Nuxt Error'), {
|
||||
borderColor: 'red'
|
||||
})
|
||||
}
|
||||
|
||||
export function fatalBox (message, title) {
|
||||
return errorBox(message, title || chalk.red('✖ Nuxt Fatal Error'))
|
||||
}
|
64
packages/nuxt3/src/cli/utils/index.ts
Normal file
64
packages/nuxt3/src/cli/utils/index.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import path from 'path'
|
||||
import exit from 'exit'
|
||||
|
||||
import { lock } from 'src/utils'
|
||||
import chalk from 'chalk'
|
||||
import env from 'std-env'
|
||||
import { warningBox } from './formatting'
|
||||
|
||||
export const eventsMapping = {
|
||||
add: { icon: '+', color: 'green', action: 'Created' },
|
||||
change: { icon: env.windows ? '»' : '↻', color: 'blue', action: 'Updated' },
|
||||
unlink: { icon: '-', color: 'red', action: 'Removed' }
|
||||
}
|
||||
|
||||
export function formatPath (filePath) {
|
||||
if (!filePath) {
|
||||
return
|
||||
}
|
||||
return filePath.replace(process.cwd() + path.sep, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize string argument in command
|
||||
*
|
||||
* @export
|
||||
* @param {String} argument
|
||||
* @param {*} defaultValue
|
||||
* @returns formatted argument
|
||||
*/
|
||||
export function normalizeArg (arg, defaultValue) {
|
||||
switch (arg) {
|
||||
case 'true': arg = true; break
|
||||
case '': arg = true; break
|
||||
case 'false': arg = false; break
|
||||
case undefined: arg = defaultValue; break
|
||||
}
|
||||
return arg
|
||||
}
|
||||
|
||||
export function forceExit (cmdName, timeout) {
|
||||
if (timeout !== false) {
|
||||
const exitTimeout = setTimeout(() => {
|
||||
const msg = `The command 'nuxt ${cmdName}' finished but did not exit after ${timeout}s
|
||||
This is most likely not caused by a bug in Nuxt.js
|
||||
Make sure to cleanup all timers and listeners you or your plugins/modules start.
|
||||
Nuxt.js will now force exit
|
||||
|
||||
${chalk.bold('DeprecationWarning: Starting with Nuxt version 3 this will be a fatal error')}`
|
||||
|
||||
// TODO: Change this to a fatal error in v3
|
||||
process.stderr.write(warningBox(msg))
|
||||
exit(0)
|
||||
}, timeout * 1000)
|
||||
exitTimeout.unref()
|
||||
} else {
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return lock(...args)
|
||||
}
|
18
packages/nuxt3/src/cli/utils/memory.ts
Normal file
18
packages/nuxt3/src/cli/utils/memory.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import chalk from 'chalk'
|
||||
import consola from 'consola'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
export function getMemoryUsage () {
|
||||
// https://nodejs.org/api/process.html#process_process_memoryusage
|
||||
const { heapUsed, rss } = process.memoryUsage()
|
||||
return { heap: heapUsed, rss }
|
||||
}
|
||||
|
||||
export function getFormattedMemoryUsage () {
|
||||
const { heap, rss } = getMemoryUsage()
|
||||
return `Memory usage: ${chalk.bold(prettyBytes(heap))} (RSS: ${prettyBytes(rss)})`
|
||||
}
|
||||
|
||||
export function showMemoryUsage () {
|
||||
consola.info(getFormattedMemoryUsage())
|
||||
}
|
9
packages/nuxt3/src/cli/utils/webpack.ts
Normal file
9
packages/nuxt3/src/cli/utils/webpack.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { loadNuxt } from 'src/core'
|
||||
import { getBuilder } from 'src/builder'
|
||||
|
||||
export async function getWebpackConfig(name = 'client', loadOptions = {}) {
|
||||
const nuxt = await loadNuxt(loadOptions)
|
||||
const builder = await getBuilder(nuxt)
|
||||
const { bundleBuilder } = builder
|
||||
return bundleBuilder.getWebpackConfig(name)
|
||||
}
|
76
packages/nuxt3/src/config/config/_app.ts
Normal file
76
packages/nuxt3/src/config/config/_app.ts
Normal file
@ -0,0 +1,76 @@
|
||||
export default () => ({
|
||||
vue: {
|
||||
config: {
|
||||
silent: undefined, // = !dev
|
||||
performance: undefined // = dev
|
||||
}
|
||||
},
|
||||
|
||||
vueMeta: null,
|
||||
|
||||
head: {
|
||||
meta: [],
|
||||
link: [],
|
||||
style: [],
|
||||
script: []
|
||||
},
|
||||
|
||||
fetch: {
|
||||
server: true,
|
||||
client: true
|
||||
},
|
||||
|
||||
plugins: [],
|
||||
|
||||
extendPlugins: null,
|
||||
|
||||
css: [],
|
||||
|
||||
layouts: {},
|
||||
|
||||
ErrorPage: null,
|
||||
|
||||
loading: {
|
||||
color: 'black',
|
||||
failedColor: 'red',
|
||||
height: '2px',
|
||||
throttle: 200,
|
||||
duration: 5000,
|
||||
continuous: false,
|
||||
rtl: false,
|
||||
css: true
|
||||
},
|
||||
|
||||
loadingIndicator: 'default',
|
||||
|
||||
pageTransition: {
|
||||
name: 'page',
|
||||
mode: 'out-in',
|
||||
appear: false,
|
||||
appearClass: 'appear',
|
||||
appearActiveClass: 'appear-active',
|
||||
appearToClass: 'appear-to'
|
||||
},
|
||||
|
||||
layoutTransition: {
|
||||
name: 'layout',
|
||||
mode: 'out-in'
|
||||
},
|
||||
|
||||
features: {
|
||||
store: true,
|
||||
layouts: true,
|
||||
meta: true,
|
||||
middleware: true,
|
||||
transitions: true,
|
||||
deprecations: true,
|
||||
validate: true,
|
||||
asyncData: true,
|
||||
fetch: true,
|
||||
clientOnline: true,
|
||||
clientPrefetch: true,
|
||||
clientUseUrl: false,
|
||||
componentAliases: true,
|
||||
componentClientOnly: true
|
||||
}
|
||||
})
|
92
packages/nuxt3/src/config/config/_common.ts
Normal file
92
packages/nuxt3/src/config/config/_common.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import capitalize from 'lodash/capitalize'
|
||||
import env from 'std-env'
|
||||
import { TARGETS, MODES } from 'src/utils'
|
||||
|
||||
export default () => ({
|
||||
// Env
|
||||
dev: Boolean(env.dev),
|
||||
test: Boolean(env.test),
|
||||
debug: undefined, // = dev
|
||||
env: {},
|
||||
|
||||
createRequire: undefined,
|
||||
|
||||
// Target
|
||||
target: TARGETS.server,
|
||||
|
||||
// Rendering
|
||||
ssr: true,
|
||||
|
||||
// TODO: remove in Nuxt 3
|
||||
// Mode
|
||||
mode: MODES.universal,
|
||||
modern: undefined,
|
||||
|
||||
// Modules
|
||||
modules: [],
|
||||
buildModules: [],
|
||||
_modules: [],
|
||||
|
||||
globalName: undefined,
|
||||
globals: {
|
||||
id: globalName => `__${globalName}`,
|
||||
nuxt: globalName => `$${globalName}`,
|
||||
context: globalName => `__${globalName.toUpperCase()}__`,
|
||||
pluginPrefix: globalName => globalName,
|
||||
readyCallback: globalName => `on${capitalize(globalName)}Ready`,
|
||||
loadedCallback: globalName => `_on${capitalize(globalName)}Loaded`
|
||||
},
|
||||
|
||||
// Server
|
||||
serverMiddleware: [],
|
||||
|
||||
// Dirs and extensions
|
||||
_nuxtConfigFile: undefined,
|
||||
srcDir: undefined,
|
||||
buildDir: '.nuxt',
|
||||
modulesDir: [
|
||||
'node_modules'
|
||||
],
|
||||
dir: {
|
||||
assets: 'assets',
|
||||
app: 'app',
|
||||
layouts: 'layouts',
|
||||
middleware: 'middleware',
|
||||
pages: 'pages',
|
||||
static: 'static',
|
||||
store: 'store'
|
||||
},
|
||||
extensions: [],
|
||||
styleExtensions: ['css', 'pcss', 'postcss', 'styl', 'stylus', 'scss', 'sass', 'less'],
|
||||
alias: {},
|
||||
|
||||
// Ignores
|
||||
ignoreOptions: undefined,
|
||||
ignorePrefix: '-',
|
||||
ignore: [
|
||||
'**/*.test.*',
|
||||
'**/*.spec.*'
|
||||
],
|
||||
|
||||
// Watch
|
||||
watch: [],
|
||||
watchers: {
|
||||
rewatchOnRawEvents: undefined,
|
||||
webpack: {
|
||||
aggregateTimeout: 1000
|
||||
},
|
||||
chokidar: {
|
||||
ignoreInitial: true
|
||||
}
|
||||
},
|
||||
|
||||
// Editor
|
||||
editor: undefined,
|
||||
|
||||
// Hooks
|
||||
hooks: null,
|
||||
|
||||
// runtimeConfig
|
||||
privateRuntimeConfig: {},
|
||||
publicRuntimeConfig: {}
|
||||
})
|
130
packages/nuxt3/src/config/config/build.ts
Normal file
130
packages/nuxt3/src/config/config/build.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import env from 'std-env'
|
||||
|
||||
export default () => ({
|
||||
quiet: Boolean(env.ci || env.test),
|
||||
analyze: false,
|
||||
profile: process.argv.includes('--profile'),
|
||||
extractCSS: false,
|
||||
cssSourceMap: undefined,
|
||||
ssr: undefined,
|
||||
parallel: false,
|
||||
cache: false,
|
||||
standalone: false,
|
||||
publicPath: '/_nuxt/',
|
||||
serverURLPolyfill: 'url',
|
||||
filenames: {
|
||||
// { isDev, isClient, isServer }
|
||||
app: ({ isDev, isModern }) => isDev ? `[name]${isModern ? '.modern' : ''}.js` : `[name].[contenthash:7]${isModern ? '.modern' : ''}.js`,
|
||||
chunk: ({ isDev, isModern }) => isDev ? `[name]${isModern ? '.modern' : ''}.js` : `[name].[contenthash:7]${isModern ? '.modern' : ''}.js`,
|
||||
css: ({ isDev }) => isDev ? '[name].css' : '[name].[contenthash:7].css',
|
||||
img: ({ isDev }) => isDev ? '[path][name].[ext]' : 'img/[name].[contenthash:7].[ext]',
|
||||
font: ({ isDev }) => isDev ? '[path][name].[ext]' : 'fonts/[name].[contenthash:7].[ext]',
|
||||
video: ({ isDev }) => isDev ? '[path][name].[ext]' : 'videos/[name].[contenthash:7].[ext]'
|
||||
},
|
||||
loaders: {
|
||||
file: {},
|
||||
fontUrl: { limit: 1000 },
|
||||
imgUrl: { limit: 1000 },
|
||||
pugPlain: {},
|
||||
vue: {
|
||||
transformAssetUrls: {
|
||||
video: 'src',
|
||||
source: 'src',
|
||||
object: 'src',
|
||||
embed: 'src'
|
||||
}
|
||||
},
|
||||
css: {},
|
||||
cssModules: {
|
||||
modules: {
|
||||
localIdentName: '[local]_[hash:base64:5]'
|
||||
}
|
||||
},
|
||||
less: {},
|
||||
sass: {
|
||||
sassOptions: {
|
||||
indentedSyntax: true
|
||||
}
|
||||
},
|
||||
scss: {},
|
||||
stylus: {},
|
||||
vueStyle: {}
|
||||
},
|
||||
styleResources: {},
|
||||
plugins: [],
|
||||
terser: {},
|
||||
hardSource: false,
|
||||
aggressiveCodeRemoval: false,
|
||||
optimizeCSS: undefined,
|
||||
optimization: {
|
||||
runtimeChunk: 'single',
|
||||
minimize: undefined,
|
||||
minimizer: undefined,
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
name: undefined,
|
||||
cacheGroups: {
|
||||
default: {
|
||||
name: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
splitChunks: {
|
||||
layouts: false,
|
||||
pages: true,
|
||||
commons: true
|
||||
},
|
||||
babel: {
|
||||
configFile: false,
|
||||
babelrc: false,
|
||||
cacheDirectory: undefined
|
||||
},
|
||||
transpile: [], // Name of NPM packages to be transpiled
|
||||
postcss: {
|
||||
preset: {
|
||||
// https://cssdb.org/#staging-process
|
||||
stage: 2
|
||||
}
|
||||
},
|
||||
html: {
|
||||
minify: {
|
||||
collapseBooleanAttributes: true,
|
||||
decodeEntities: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
processConditionalComments: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeRedundantAttributes: true,
|
||||
trimCustomFragments: true,
|
||||
useShortDoctype: true
|
||||
}
|
||||
},
|
||||
|
||||
template: undefined,
|
||||
templates: [],
|
||||
|
||||
watch: [],
|
||||
devMiddleware: {},
|
||||
hotMiddleware: {},
|
||||
|
||||
stats: {
|
||||
excludeAssets: [
|
||||
/.map$/,
|
||||
/index\..+\.html$/,
|
||||
/vue-ssr-(client|modern)-manifest.json/
|
||||
]
|
||||
},
|
||||
friendlyErrors: true,
|
||||
additionalExtensions: [],
|
||||
warningIgnoreFilters: [],
|
||||
|
||||
followSymlinks: false,
|
||||
|
||||
loadingScreen: {},
|
||||
indicator: {
|
||||
position: 'bottom-right',
|
||||
backgroundColor: '#2E495E',
|
||||
color: '#00C48D'
|
||||
}
|
||||
})
|
4
packages/nuxt3/src/config/config/cli.ts
Normal file
4
packages/nuxt3/src/config/config/cli.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default () => ({
|
||||
badgeMessages: [],
|
||||
bannerColor: 'green'
|
||||
})
|
17
packages/nuxt3/src/config/config/generate.ts
Normal file
17
packages/nuxt3/src/config/config/generate.ts
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
export default () => ({
|
||||
dir: 'dist',
|
||||
routes: [],
|
||||
exclude: [],
|
||||
concurrency: 500,
|
||||
interval: 0,
|
||||
subFolders: true,
|
||||
fallback: '200.html',
|
||||
crawler: true,
|
||||
staticAssets: {
|
||||
base: undefined, // Default: "/_nuxt/static:
|
||||
versionBase: undefined, // Default: "_nuxt/static/{version}""
|
||||
dir: 'static',
|
||||
version: undefined // Default: "{timeStampSec}"
|
||||
}
|
||||
})
|
33
packages/nuxt3/src/config/config/index.ts
Normal file
33
packages/nuxt3/src/config/config/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
import _app from './_app'
|
||||
import _common from './_common'
|
||||
|
||||
import build from './build'
|
||||
import messages from './messages'
|
||||
import modes from './modes'
|
||||
import render from './render'
|
||||
import router from './router'
|
||||
import server from './server'
|
||||
import cli from './cli'
|
||||
import generate from './generate'
|
||||
|
||||
export const defaultNuxtConfigFile = 'nuxt.config'
|
||||
|
||||
export function getDefaultNuxtConfig (options = {}) {
|
||||
if (!options.env) {
|
||||
options.env = process.env
|
||||
}
|
||||
|
||||
return {
|
||||
..._app(),
|
||||
..._common(),
|
||||
build: build(),
|
||||
messages: messages(),
|
||||
modes: modes(),
|
||||
render: render(),
|
||||
router: router(),
|
||||
server: server(options),
|
||||
cli: cli(),
|
||||
generate: generate()
|
||||
}
|
||||
}
|
12
packages/nuxt3/src/config/config/messages.ts
Normal file
12
packages/nuxt3/src/config/config/messages.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export default () => ({
|
||||
loading: 'Loading...',
|
||||
error_404: 'This page could not be found',
|
||||
server_error: 'Server error',
|
||||
nuxtjs: 'Nuxt.js',
|
||||
back_to_home: 'Back to the home page',
|
||||
server_error_details:
|
||||
'An error occurred in the application and your page could not be served. If you are the application owner, check your logs for details.',
|
||||
client_error: 'Error',
|
||||
client_error_details:
|
||||
'An error occurred while rendering the page. Check developer tools console for details.'
|
||||
})
|
20
packages/nuxt3/src/config/config/modes.ts
Normal file
20
packages/nuxt3/src/config/config/modes.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { MODES } from 'src/utils'
|
||||
|
||||
export default () => ({
|
||||
[MODES.universal]: {
|
||||
build: {
|
||||
ssr: true
|
||||
},
|
||||
render: {
|
||||
ssr: true
|
||||
}
|
||||
},
|
||||
[MODES.spa]: {
|
||||
build: {
|
||||
ssr: false
|
||||
},
|
||||
render: {
|
||||
ssr: false
|
||||
}
|
||||
}
|
||||
})
|
45
packages/nuxt3/src/config/config/render.ts
Normal file
45
packages/nuxt3/src/config/config/render.ts
Normal file
@ -0,0 +1,45 @@
|
||||
// TODO: Refactor @nuxt/server related options into `server.js`
|
||||
|
||||
export default () => ({
|
||||
bundleRenderer: {
|
||||
shouldPrefetch: () => false,
|
||||
shouldPreload: (fileWithoutQuery, asType) => ['script', 'style'].includes(asType),
|
||||
runInNewContext: undefined
|
||||
},
|
||||
crossorigin: undefined,
|
||||
resourceHints: true,
|
||||
ssr: undefined,
|
||||
ssrLog: undefined,
|
||||
http2: {
|
||||
push: false,
|
||||
shouldPush: null,
|
||||
pushAssets: null
|
||||
},
|
||||
static: {
|
||||
prefix: true
|
||||
},
|
||||
compressor: {
|
||||
threshold: 0
|
||||
},
|
||||
etag: {
|
||||
weak: false
|
||||
},
|
||||
csp: false,
|
||||
dist: {
|
||||
// Don't serve index.html template
|
||||
index: false,
|
||||
// 1 year in production
|
||||
maxAge: '1y'
|
||||
},
|
||||
// https://github.com/nuxt/serve-placeholder
|
||||
fallback: {
|
||||
dist: {},
|
||||
static: {
|
||||
skipUnknown: true,
|
||||
handlers: {
|
||||
'.htm': false,
|
||||
'.html': false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
18
packages/nuxt3/src/config/config/router.ts
Normal file
18
packages/nuxt3/src/config/config/router.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export default () => ({
|
||||
mode: 'history',
|
||||
base: '/',
|
||||
routes: [],
|
||||
routeNameSplitter: '-',
|
||||
middleware: [],
|
||||
linkActiveClass: 'nuxt-link-active',
|
||||
linkExactActiveClass: 'nuxt-link-exact-active',
|
||||
linkPrefetchedClass: false,
|
||||
extendRoutes: null,
|
||||
scrollBehavior: null,
|
||||
parseQuery: false,
|
||||
stringifyQuery: false,
|
||||
fallback: false,
|
||||
prefetchLinks: true,
|
||||
prefetchPayloads: true,
|
||||
trailingSlash: undefined
|
||||
})
|
14
packages/nuxt3/src/config/config/server.ts
Normal file
14
packages/nuxt3/src/config/config/server.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export default ({ env = {} } = {}) => ({
|
||||
https: false,
|
||||
port: env.NUXT_PORT ||
|
||||
env.PORT ||
|
||||
env.npm_package_config_nuxt_port ||
|
||||
3000,
|
||||
host: env.NUXT_HOST ||
|
||||
env.HOST ||
|
||||
env.npm_package_config_nuxt_host ||
|
||||
'localhost',
|
||||
socket: env.UNIX_SOCKET ||
|
||||
env.npm_package_config_unix_socket,
|
||||
timing: false
|
||||
})
|
3
packages/nuxt3/src/config/index.ts
Normal file
3
packages/nuxt3/src/config/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { defaultNuxtConfigFile, getDefaultNuxtConfig } from './config'
|
||||
export { getNuxtConfig } from './options'
|
||||
export { loadNuxtConfig } from './load'
|
187
packages/nuxt3/src/config/load.ts
Normal file
187
packages/nuxt3/src/config/load.ts
Normal file
@ -0,0 +1,187 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import defu from 'defu'
|
||||
import consola from 'consola'
|
||||
import dotenv from 'dotenv'
|
||||
import { clearRequireCache, scanRequireTree } from 'src/utils'
|
||||
import jiti from 'jiti'
|
||||
import _createRequire from 'create-require'
|
||||
import destr from 'destr'
|
||||
import * as rc from 'rc9'
|
||||
import { defaultNuxtConfigFile } from './config'
|
||||
|
||||
const isJest = typeof jest !== 'undefined'
|
||||
|
||||
export async function loadNuxtConfig ({
|
||||
rootDir = '.',
|
||||
envConfig = {},
|
||||
configFile = defaultNuxtConfigFile,
|
||||
configContext = {},
|
||||
configOverrides = {},
|
||||
createRequire = module => isJest ? _createRequire(module.filename) : jiti(module.filename)
|
||||
} = {}) {
|
||||
rootDir = path.resolve(rootDir)
|
||||
|
||||
let options = {}
|
||||
|
||||
try {
|
||||
configFile = require.resolve(path.resolve(rootDir, configFile))
|
||||
} catch (e) {
|
||||
if (e.code !== 'MODULE_NOT_FOUND') {
|
||||
throw (e)
|
||||
} else if (configFile !== defaultNuxtConfigFile) {
|
||||
consola.fatal('Config file not found: ' + configFile)
|
||||
}
|
||||
// Skip configFile if cannot resolve
|
||||
configFile = undefined
|
||||
}
|
||||
|
||||
// Load env
|
||||
envConfig = {
|
||||
dotenv: '.env',
|
||||
env: process.env,
|
||||
expand: true,
|
||||
...envConfig
|
||||
}
|
||||
|
||||
const env = loadEnv(envConfig, rootDir)
|
||||
|
||||
// Fill process.env so it is accessible in nuxt.config
|
||||
for (const key in env) {
|
||||
if (!key.startsWith('_') && envConfig.env[key] === undefined) {
|
||||
envConfig.env[key] = env[key]
|
||||
}
|
||||
}
|
||||
|
||||
if (configFile) {
|
||||
// Clear cache
|
||||
clearRequireCache(configFile)
|
||||
const _require = createRequire(module)
|
||||
options = _require(configFile) || {}
|
||||
if (options.default) {
|
||||
options = options.default
|
||||
}
|
||||
|
||||
if (typeof options === 'function') {
|
||||
try {
|
||||
options = await options(configContext)
|
||||
if (options.default) {
|
||||
options = options.default
|
||||
}
|
||||
} catch (error) {
|
||||
consola.error(error)
|
||||
consola.fatal('Error while fetching async configuration')
|
||||
}
|
||||
}
|
||||
|
||||
// Don't mutate options export
|
||||
options = { ...options }
|
||||
|
||||
// Keep _nuxtConfigFile for watching
|
||||
options._nuxtConfigFile = configFile
|
||||
|
||||
// Keep all related files for watching
|
||||
options._nuxtConfigFiles = Array.from(scanRequireTree(configFile))
|
||||
if (!options._nuxtConfigFiles.includes(configFile)) {
|
||||
options._nuxtConfigFiles.unshift(configFile)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.rootDir !== 'string') {
|
||||
options.rootDir = rootDir
|
||||
}
|
||||
|
||||
// Load Combine configs
|
||||
// Priority: configOverrides > nuxtConfig > .nuxtrc > .nuxtrc (global)
|
||||
options = defu(
|
||||
configOverrides,
|
||||
options,
|
||||
rc.read({ name: '.nuxtrc', dir: options.rootDir }),
|
||||
rc.readUser('.nuxtrc')
|
||||
)
|
||||
|
||||
// Load env to options._env
|
||||
options._env = env
|
||||
options._envConfig = envConfig
|
||||
if (configContext) { configContext.env = env }
|
||||
|
||||
// Expand and interpolate runtimeConfig from _env
|
||||
if (envConfig.expand) {
|
||||
for (const c of ['publicRuntimeConfig', 'privateRuntimeConfig']) {
|
||||
if (options[c]) {
|
||||
if (typeof options[c] === 'function') {
|
||||
options[c] = options[c](env)
|
||||
}
|
||||
expand(options[c], env, destr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
function loadEnv (envConfig, rootDir = process.cwd()) {
|
||||
const env = Object.create(null)
|
||||
|
||||
// Read dotenv
|
||||
if (envConfig.dotenv) {
|
||||
envConfig.dotenv = path.resolve(rootDir, envConfig.dotenv)
|
||||
if (fs.existsSync(envConfig.dotenv)) {
|
||||
const parsed = dotenv.parse(fs.readFileSync(envConfig.dotenv, 'utf-8'))
|
||||
Object.assign(env, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply process.env
|
||||
if (!envConfig.env._applied) {
|
||||
Object.assign(env, envConfig.env)
|
||||
envConfig.env._applied = true
|
||||
}
|
||||
|
||||
// Interpolate env
|
||||
if (envConfig.expand) {
|
||||
expand(env)
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// Based on https://github.com/motdotla/dotenv-expand
|
||||
function expand (target, source = {}, parse = v => v) {
|
||||
function getValue (key) {
|
||||
// Source value 'wins' over target value
|
||||
return source[key] !== undefined ? source[key] : target[key]
|
||||
}
|
||||
|
||||
function interpolate (value) {
|
||||
if (typeof value !== 'string') {
|
||||
return value
|
||||
}
|
||||
const matches = value.match(/(.?\${?(?:[a-zA-Z0-9_:]+)?}?)/g) || []
|
||||
return parse(matches.reduce((newValue, match) => {
|
||||
const parts = /(.?)\${?([a-zA-Z0-9_:]+)?}?/g.exec(match)
|
||||
const prefix = parts[1]
|
||||
|
||||
let value, replacePart
|
||||
|
||||
if (prefix === '\\') {
|
||||
replacePart = parts[0]
|
||||
value = replacePart.replace('\\$', '$')
|
||||
} else {
|
||||
const key = parts[2]
|
||||
replacePart = parts[0].substring(prefix.length)
|
||||
|
||||
value = getValue(key)
|
||||
|
||||
// Resolve recursive interpolations
|
||||
value = interpolate(value)
|
||||
}
|
||||
|
||||
return newValue.replace(replacePart, value)
|
||||
}, value))
|
||||
}
|
||||
|
||||
for (const key in target) {
|
||||
target[key] = interpolate(getValue(key))
|
||||
}
|
||||
}
|
494
packages/nuxt3/src/config/options.ts
Normal file
494
packages/nuxt3/src/config/options.ts
Normal file
@ -0,0 +1,494 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import defaultsDeep from 'lodash/defaultsDeep'
|
||||
import defu from 'defu'
|
||||
import pick from 'lodash/pick'
|
||||
import uniq from 'lodash/uniq'
|
||||
import consola from 'consola'
|
||||
import destr from 'destr'
|
||||
import { TARGETS, MODES, guardDir, isNonEmptyString, isPureObject, isUrl, getMainModule, urlJoin, getPKG } from 'src/utils'
|
||||
import { defaultNuxtConfigFile, getDefaultNuxtConfig } from './config'
|
||||
|
||||
export function getNuxtConfig (_options) {
|
||||
// Prevent duplicate calls
|
||||
if (_options.__normalized__) {
|
||||
return _options
|
||||
}
|
||||
|
||||
// Clone options to prevent unwanted side-effects
|
||||
const options = Object.assign({}, _options)
|
||||
options.__normalized__ = true
|
||||
|
||||
// Normalize options
|
||||
if (options.loading === true) {
|
||||
delete options.loading
|
||||
}
|
||||
|
||||
if (
|
||||
options.router &&
|
||||
options.router.middleware &&
|
||||
!Array.isArray(options.router.middleware)
|
||||
) {
|
||||
options.router.middleware = [options.router.middleware]
|
||||
}
|
||||
|
||||
if (options.router && typeof options.router.base === 'string') {
|
||||
options._routerBaseSpecified = true
|
||||
}
|
||||
|
||||
// TODO: Remove for Nuxt 3
|
||||
// router.scrollBehavior -> app/router.scrollBehavior.js
|
||||
if (options.router && typeof options.router.scrollBehavior !== 'undefined') {
|
||||
consola.warn('`router.scrollBehavior` property is deprecated in favor of using `~/app/router.scrollBehavior.js` file, learn more: https://nuxtjs.org/api/configuration-router#scrollbehavior')
|
||||
}
|
||||
|
||||
// TODO: Remove for Nuxt 3
|
||||
// transition -> pageTransition
|
||||
if (typeof options.transition !== 'undefined') {
|
||||
consola.warn('`transition` property is deprecated in favor of `pageTransition` and will be removed in Nuxt 3')
|
||||
options.pageTransition = options.transition
|
||||
delete options.transition
|
||||
}
|
||||
|
||||
if (typeof options.pageTransition === 'string') {
|
||||
options.pageTransition = { name: options.pageTransition }
|
||||
}
|
||||
|
||||
if (typeof options.layoutTransition === 'string') {
|
||||
options.layoutTransition = { name: options.layoutTransition }
|
||||
}
|
||||
|
||||
if (typeof options.extensions === 'string') {
|
||||
options.extensions = [options.extensions]
|
||||
}
|
||||
|
||||
options.globalName = (isNonEmptyString(options.globalName) && /^[a-zA-Z]+$/.test(options.globalName))
|
||||
? options.globalName.toLowerCase()
|
||||
// use `` for preventing replacing to nuxt-edge
|
||||
: 'nuxt'
|
||||
|
||||
// Resolve rootDir
|
||||
options.rootDir = isNonEmptyString(options.rootDir) ? path.resolve(options.rootDir) : process.cwd()
|
||||
|
||||
// Apply defaults by ${buildDir}/dist/build.config.js
|
||||
// TODO: Unsafe operation.
|
||||
// const buildDir = options.buildDir || defaults.buildDir
|
||||
// const buildConfig = resolve(options.rootDir, buildDir, 'build.config.js')
|
||||
// if (existsSync(buildConfig)) {
|
||||
// defaultsDeep(options, require(buildConfig))
|
||||
// }
|
||||
|
||||
// Apply defaults
|
||||
const nuxtConfig = getDefaultNuxtConfig()
|
||||
|
||||
nuxtConfig.build._publicPath = nuxtConfig.build.publicPath
|
||||
|
||||
// Fall back to default if publicPath is falsy
|
||||
if (options.build && !options.build.publicPath) {
|
||||
options.build.publicPath = undefined
|
||||
}
|
||||
|
||||
defaultsDeep(options, nuxtConfig)
|
||||
|
||||
// Target
|
||||
options.target = options.target || 'server'
|
||||
if (!Object.values(TARGETS).includes(options.target)) {
|
||||
consola.warn(`Unknown target: ${options.target}. Falling back to server`)
|
||||
options.target = 'server'
|
||||
}
|
||||
|
||||
// SSR root option
|
||||
if (options.ssr === false) {
|
||||
options.mode = MODES.spa
|
||||
}
|
||||
|
||||
// Apply mode preset
|
||||
const modePreset = options.modes[options.mode || MODES.universal]
|
||||
|
||||
if (!modePreset) {
|
||||
consola.warn(`Unknown mode: ${options.mode}. Falling back to ${MODES.universal}`)
|
||||
}
|
||||
defaultsDeep(options, modePreset || options.modes[MODES.universal])
|
||||
|
||||
// Sanitize router.base
|
||||
if (!/\/$/.test(options.router.base)) {
|
||||
options.router.base += '/'
|
||||
}
|
||||
|
||||
// Alias export to generate
|
||||
// TODO: switch to export by default for nuxt3
|
||||
if (options.export) {
|
||||
options.generate = defu(options.export, options.generate)
|
||||
}
|
||||
exports.export = options.generate
|
||||
|
||||
// Check srcDir and generate.dir existence
|
||||
const hasSrcDir = isNonEmptyString(options.srcDir)
|
||||
const hasGenerateDir = isNonEmptyString(options.generate.dir)
|
||||
|
||||
// Resolve srcDir
|
||||
options.srcDir = hasSrcDir
|
||||
? path.resolve(options.rootDir, options.srcDir)
|
||||
: options.rootDir
|
||||
|
||||
// Resolve buildDir
|
||||
options.buildDir = path.resolve(options.rootDir, options.buildDir)
|
||||
|
||||
// Aliases
|
||||
const { rootDir, srcDir, dir: { assets: assetsDir, static: staticDir } } = options
|
||||
options.alias = {
|
||||
'~~': rootDir,
|
||||
'@@': rootDir,
|
||||
'~': srcDir,
|
||||
'@': srcDir,
|
||||
[assetsDir]: path.join(srcDir, assetsDir),
|
||||
[staticDir]: path.join(srcDir, staticDir),
|
||||
...options.alias
|
||||
}
|
||||
|
||||
// Default value for _nuxtConfigFile
|
||||
if (!options._nuxtConfigFile) {
|
||||
options._nuxtConfigFile = path.resolve(options.rootDir, `${defaultNuxtConfigFile}.js`)
|
||||
}
|
||||
|
||||
if (!options._nuxtConfigFiles) {
|
||||
options._nuxtConfigFiles = [
|
||||
options._nuxtConfigFile
|
||||
]
|
||||
}
|
||||
|
||||
// Watch for config file changes
|
||||
options.watch.push(...options._nuxtConfigFiles)
|
||||
|
||||
// Protect rootDir against buildDir
|
||||
guardDir(options, 'rootDir', 'buildDir')
|
||||
|
||||
if (hasGenerateDir) {
|
||||
// Resolve generate.dir
|
||||
options.generate.dir = path.resolve(options.rootDir, options.generate.dir)
|
||||
|
||||
// Protect rootDir against buildDir
|
||||
guardDir(options, 'rootDir', 'generate.dir')
|
||||
}
|
||||
|
||||
if (hasSrcDir) {
|
||||
// Protect srcDir against buildDir
|
||||
guardDir(options, 'srcDir', 'buildDir')
|
||||
|
||||
if (hasGenerateDir) {
|
||||
// Protect srcDir against generate.dir
|
||||
guardDir(options, 'srcDir', 'generate.dir')
|
||||
}
|
||||
}
|
||||
|
||||
// Populate modulesDir
|
||||
options.modulesDir = uniq(
|
||||
getMainModule().paths.concat(
|
||||
[].concat(options.modulesDir).map(dir => path.resolve(options.rootDir, dir))
|
||||
)
|
||||
)
|
||||
|
||||
const mandatoryExtensions = ['js', 'mjs']
|
||||
|
||||
options.extensions = mandatoryExtensions
|
||||
.filter(ext => !options.extensions.includes(ext))
|
||||
.concat(options.extensions)
|
||||
|
||||
// If app.html is defined, set the template path to the user template
|
||||
if (options.appTemplatePath === undefined) {
|
||||
options.appTemplatePath = path.resolve(options.buildDir, 'views/app.template.html')
|
||||
if (fs.existsSync(path.join(options.srcDir, 'app.html'))) {
|
||||
options.appTemplatePath = path.join(options.srcDir, 'app.html')
|
||||
}
|
||||
} else {
|
||||
options.appTemplatePath = path.resolve(options.srcDir, options.appTemplatePath)
|
||||
}
|
||||
|
||||
options.build.publicPath = options.build.publicPath.replace(/([^/])$/, '$1/')
|
||||
options.build._publicPath = options.build._publicPath.replace(/([^/])$/, '$1/')
|
||||
|
||||
// Ignore publicPath on dev
|
||||
if (options.dev && isUrl(options.build.publicPath)) {
|
||||
options.build.publicPath = options.build._publicPath
|
||||
}
|
||||
|
||||
// If store defined, update store options to true unless explicitly disabled
|
||||
if (
|
||||
options.store !== false &&
|
||||
fs.existsSync(path.join(options.srcDir, options.dir.store)) &&
|
||||
fs.readdirSync(path.join(options.srcDir, options.dir.store))
|
||||
.find(filename => filename !== 'README.md' && filename[0] !== '.')
|
||||
) {
|
||||
options.store = true
|
||||
}
|
||||
|
||||
// SPA loadingIndicator
|
||||
if (options.loadingIndicator) {
|
||||
// Normalize loadingIndicator
|
||||
if (!isPureObject(options.loadingIndicator)) {
|
||||
options.loadingIndicator = { name: options.loadingIndicator }
|
||||
}
|
||||
|
||||
// Apply defaults
|
||||
options.loadingIndicator = Object.assign(
|
||||
{
|
||||
name: 'default',
|
||||
color: (options.loading && options.loading.color) || '#D3D3D3',
|
||||
color2: '#F5F5F5',
|
||||
background: (options.manifest && options.manifest.theme_color) || 'white',
|
||||
dev: options.dev,
|
||||
loading: options.messages.loading
|
||||
},
|
||||
options.loadingIndicator
|
||||
)
|
||||
}
|
||||
|
||||
// Debug errors
|
||||
if (options.debug === undefined) {
|
||||
options.debug = options.dev
|
||||
}
|
||||
|
||||
// Validate that etag.hash is a function, if not unset it
|
||||
if (options.render.etag) {
|
||||
const { hash } = options.render.etag
|
||||
if (hash) {
|
||||
const isFn = typeof hash === 'function'
|
||||
if (!isFn) {
|
||||
options.render.etag.hash = undefined
|
||||
|
||||
if (options.dev) {
|
||||
consola.warn(`render.etag.hash should be a function, received ${typeof hash} instead`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply default hash to CSP option
|
||||
if (options.render.csp) {
|
||||
options.render.csp = defu(options.render.csp, {
|
||||
hashAlgorithm: 'sha256',
|
||||
allowedSources: undefined,
|
||||
policies: undefined,
|
||||
addMeta: Boolean(options.target === TARGETS.static),
|
||||
unsafeInlineCompatibility: false,
|
||||
reportOnly: options.debug
|
||||
})
|
||||
|
||||
// TODO: Remove this if statement in Nuxt 3, we will stop supporting this typo (more on: https://github.com/nuxt/nuxt.js/pull/6583)
|
||||
if (options.render.csp.unsafeInlineCompatiblity) {
|
||||
consola.warn('Using `unsafeInlineCompatiblity` is deprecated and will be removed in Nuxt 3. Use `unsafeInlineCompatibility` instead.')
|
||||
options.render.csp.unsafeInlineCompatibility = options.render.csp.unsafeInlineCompatiblity
|
||||
delete options.render.csp.unsafeInlineCompatiblity
|
||||
}
|
||||
}
|
||||
|
||||
// cssSourceMap
|
||||
if (options.build.cssSourceMap === undefined) {
|
||||
options.build.cssSourceMap = options.dev
|
||||
}
|
||||
|
||||
const babelConfig = options.build.babel
|
||||
// babel cacheDirectory
|
||||
if (babelConfig.cacheDirectory === undefined) {
|
||||
babelConfig.cacheDirectory = options.dev
|
||||
}
|
||||
|
||||
// TODO: remove this warn in Nuxt 3
|
||||
if (Array.isArray(babelConfig.presets)) {
|
||||
const warnPreset = (presetName) => {
|
||||
const oldPreset = '@nuxtjs/babel-preset-app'
|
||||
const newPreset = '@nuxt/babel-preset-app'
|
||||
if (presetName.includes(oldPreset)) {
|
||||
presetName = presetName.replace(oldPreset, newPreset)
|
||||
consola.warn('@nuxtjs/babel-preset-app has been deprecated, please use @nuxt/babel-preset-app.')
|
||||
}
|
||||
return presetName
|
||||
}
|
||||
babelConfig.presets = babelConfig.presets.map((preset) => {
|
||||
const hasOptions = Array.isArray(preset)
|
||||
if (hasOptions) {
|
||||
preset[0] = warnPreset(preset[0])
|
||||
} else if (typeof preset === 'string') {
|
||||
preset = warnPreset(preset)
|
||||
}
|
||||
return preset
|
||||
})
|
||||
}
|
||||
|
||||
// Vue config
|
||||
const vueConfig = options.vue.config
|
||||
|
||||
if (vueConfig.silent === undefined) {
|
||||
vueConfig.silent = !options.dev
|
||||
}
|
||||
if (vueConfig.performance === undefined) {
|
||||
vueConfig.performance = options.dev
|
||||
}
|
||||
|
||||
// merge custom env with variables
|
||||
const eligibleEnvVariables = pick(process.env, Object.keys(process.env).filter(k => k.startsWith('NUXT_ENV_')))
|
||||
Object.assign(options.env, eligibleEnvVariables)
|
||||
|
||||
// Normalize ignore
|
||||
options.ignore = options.ignore ? [].concat(options.ignore) : []
|
||||
|
||||
// Append ignorePrefix glob to ignore
|
||||
if (typeof options.ignorePrefix === 'string') {
|
||||
options.ignore.push(`**/${options.ignorePrefix}*.*`)
|
||||
}
|
||||
|
||||
// Compression middleware legacy
|
||||
if (options.render.gzip) {
|
||||
consola.warn('render.gzip is deprecated and will be removed in a future version! Please switch to render.compressor')
|
||||
options.render.compressor = options.render.gzip
|
||||
delete options.render.gzip
|
||||
}
|
||||
|
||||
// If no server-side rendering, add appear true transition
|
||||
if (options.render.ssr === false && options.pageTransition) {
|
||||
options.pageTransition.appear = true
|
||||
}
|
||||
|
||||
options.render.ssrLog = options.dev
|
||||
? options.render.ssrLog === undefined || options.render.ssrLog
|
||||
: false
|
||||
|
||||
// We assume the SPA fallback path is 404.html (for GitHub Pages, Surge, etc.)
|
||||
if (options.generate.fallback === true) {
|
||||
options.generate.fallback = '404.html'
|
||||
}
|
||||
|
||||
if (options.build.stats === 'none' || options.build.quiet === true) {
|
||||
options.build.stats = false
|
||||
}
|
||||
|
||||
// Vendor backward compatibility with nuxt 1.x
|
||||
if (typeof options.build.vendor !== 'undefined') {
|
||||
delete options.build.vendor
|
||||
consola.warn('vendor has been deprecated due to webpack4 optimization')
|
||||
}
|
||||
|
||||
// Disable CSS extraction due to incompatibility with thread-loader
|
||||
if (options.build.extractCSS && options.build.parallel) {
|
||||
options.build.parallel = false
|
||||
consola.warn('extractCSS cannot work with parallel build due to limited work pool in thread-loader')
|
||||
}
|
||||
|
||||
// build.extractCSS.allChunks has no effect
|
||||
if (typeof options.build.extractCSS.allChunks !== 'undefined') {
|
||||
consola.warn('build.extractCSS.allChunks has no effect from v2.0.0. Please use build.optimization.splitChunks settings instead.')
|
||||
}
|
||||
|
||||
// devModules has been renamed to buildModules
|
||||
if (typeof options.devModules !== 'undefined') {
|
||||
consola.warn('`devModules` has been renamed to `buildModules` and will be removed in Nuxt 3.')
|
||||
options.buildModules.push(...options.devModules)
|
||||
delete options.devModules
|
||||
}
|
||||
|
||||
// Enable minimize for production builds
|
||||
if (options.build.optimization.minimize === undefined) {
|
||||
options.build.optimization.minimize = !options.dev
|
||||
}
|
||||
|
||||
// Enable optimizeCSS only when extractCSS is enabled
|
||||
if (options.build.optimizeCSS === undefined) {
|
||||
options.build.optimizeCSS = options.build.extractCSS ? {} : false
|
||||
}
|
||||
|
||||
const { loaders } = options.build
|
||||
const vueLoader = loaders.vue
|
||||
if (vueLoader.productionMode === undefined) {
|
||||
vueLoader.productionMode = !options.dev
|
||||
}
|
||||
const styleLoaders = [
|
||||
'css', 'cssModules', 'less',
|
||||
'sass', 'scss', 'stylus', 'vueStyle'
|
||||
]
|
||||
for (const name of styleLoaders) {
|
||||
const loader = loaders[name]
|
||||
if (loader && loader.sourceMap === undefined) {
|
||||
loader.sourceMap = Boolean(options.build.cssSourceMap)
|
||||
}
|
||||
}
|
||||
|
||||
options.build.transpile = [].concat(options.build.transpile || [])
|
||||
|
||||
if (options.build.quiet === true) {
|
||||
consola.level = 0
|
||||
}
|
||||
|
||||
// Use runInNewContext for dev mode by default
|
||||
const { bundleRenderer } = options.render
|
||||
if (typeof bundleRenderer.runInNewContext === 'undefined') {
|
||||
bundleRenderer.runInNewContext = options.dev
|
||||
}
|
||||
|
||||
// TODO: Remove this if statement in Nuxt 3
|
||||
if (options.build.crossorigin) {
|
||||
consola.warn('Using `build.crossorigin` is deprecated and will be removed in Nuxt 3. Please use `render.crossorigin` instead.')
|
||||
options.render.crossorigin = options.build.crossorigin
|
||||
delete options.build.crossorigin
|
||||
}
|
||||
|
||||
const { timing } = options.server
|
||||
if (timing) {
|
||||
options.server.timing = { total: true, ...timing }
|
||||
}
|
||||
|
||||
if (isPureObject(options.serverMiddleware)) {
|
||||
options.serverMiddleware = Object.entries(options.serverMiddleware)
|
||||
.map(([path, handler]) => ({ path, handler }))
|
||||
}
|
||||
|
||||
// Generate staticAssets
|
||||
const { staticAssets } = options.generate
|
||||
if (!staticAssets.version) {
|
||||
staticAssets.version = String(Math.round(Date.now() / 1000))
|
||||
}
|
||||
if (!staticAssets.base) {
|
||||
const publicPath = isUrl(options.build.publicPath) ? '' : options.build.publicPath // "/_nuxt" or custom CDN URL
|
||||
staticAssets.base = urlJoin(publicPath, staticAssets.dir)
|
||||
}
|
||||
if (!staticAssets.versionBase) {
|
||||
staticAssets.versionBase = urlJoin(staticAssets.base, staticAssets.version)
|
||||
}
|
||||
|
||||
// createRequire factory
|
||||
if (options.createRequire === undefined) {
|
||||
const createRequire = require('create-require')
|
||||
options.createRequire = module => createRequire(module.filename)
|
||||
}
|
||||
|
||||
// ----- Builtin modules -----
|
||||
|
||||
// Loading screen
|
||||
// Force disable for production and programmatic users
|
||||
if (!options.dev || !options._cli || !getPKG('@nuxt/loading-screen')) {
|
||||
options.build.loadingScreen = false
|
||||
}
|
||||
if (options.build.loadingScreen) {
|
||||
options._modules.push(['@nuxt/loading-screen', options.build.loadingScreen])
|
||||
} else {
|
||||
// When loadingScreen is disabled we should also disable build indicator
|
||||
options.build.indicator = false
|
||||
}
|
||||
|
||||
// Components Module
|
||||
// TODO: Webpack5 support
|
||||
// if (!options._start && getPKG('@nuxt/components')) {
|
||||
// options._modules.push('@nuxt/components')
|
||||
// }
|
||||
|
||||
// Nuxt Telemetry
|
||||
if (
|
||||
options.telemetry !== false &&
|
||||
!options.test &&
|
||||
!destr(process.env.NUXT_TELEMETRY_DISABLED) &&
|
||||
getPKG('@nuxt/telemetry')
|
||||
) {
|
||||
options._modules.push('@nuxt/telemetry')
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
5
packages/nuxt3/src/core/index.ts
Normal file
5
packages/nuxt3/src/core/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { default as Module } from './module'
|
||||
export { default as Nuxt } from './nuxt'
|
||||
export { default as Resolver } from './resolver'
|
||||
export { loadNuxtConfig } from 'src/config'
|
||||
export { loadNuxt } from './load'
|
40
packages/nuxt3/src/core/load.ts
Normal file
40
packages/nuxt3/src/core/load.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { loadNuxtConfig } from '../config'
|
||||
import Nuxt from './nuxt'
|
||||
|
||||
const OVERRIDES = {
|
||||
dry: { dev: false, server: false },
|
||||
dev: { dev: true, _build: true },
|
||||
build: { dev: false, server: false, _build: true },
|
||||
start: { dev: false, _start: true }
|
||||
}
|
||||
|
||||
export async function loadNuxt (loadOptions) {
|
||||
// Normalize loadOptions
|
||||
if (typeof loadOptions === 'string') {
|
||||
loadOptions = { for: loadOptions }
|
||||
}
|
||||
const { ready = true } = loadOptions
|
||||
const _for = loadOptions.for || 'dry'
|
||||
|
||||
// Get overrides
|
||||
const override = OVERRIDES[_for]
|
||||
|
||||
// Unsupported purpose
|
||||
if (!override) {
|
||||
throw new Error('Unsupported for: ' + _for)
|
||||
}
|
||||
|
||||
// Load Config
|
||||
const config = await loadNuxtConfig(loadOptions)
|
||||
|
||||
// Apply config overrides
|
||||
Object.assign(config, override)
|
||||
|
||||
// Initiate Nuxt
|
||||
const nuxt = new Nuxt(config)
|
||||
if (ready) {
|
||||
await nuxt.ready()
|
||||
}
|
||||
|
||||
return nuxt
|
||||
}
|
215
packages/nuxt3/src/core/module.ts
Normal file
215
packages/nuxt3/src/core/module.ts
Normal file
@ -0,0 +1,215 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import hash from 'hash-sum'
|
||||
import consola from 'consola'
|
||||
|
||||
import { chainFn, sequence } from 'src/utils'
|
||||
|
||||
export default class ModuleContainer {
|
||||
constructor (nuxt) {
|
||||
this.nuxt = nuxt
|
||||
this.options = nuxt.options
|
||||
this.requiredModules = {}
|
||||
|
||||
// Self bind to allow destructre from container
|
||||
for (const method of Object.getOwnPropertyNames(ModuleContainer.prototype)) {
|
||||
if (typeof this[method] === 'function') {
|
||||
this[method] = this[method].bind(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async ready () {
|
||||
// Call before hook
|
||||
await this.nuxt.callHook('modules:before', this, this.options.modules)
|
||||
|
||||
if (this.options.buildModules && !this.options._start) {
|
||||
// Load every devModule in sequence
|
||||
await sequence(this.options.buildModules, this.addModule)
|
||||
}
|
||||
|
||||
// Load every module in sequence
|
||||
await sequence(this.options.modules, this.addModule)
|
||||
|
||||
// Load ah-hoc modules last
|
||||
await sequence(this.options._modules, this.addModule)
|
||||
|
||||
// Call done hook
|
||||
await this.nuxt.callHook('modules:done', this)
|
||||
}
|
||||
|
||||
addVendor () {
|
||||
consola.warn('addVendor has been deprecated due to webpack4 optimization')
|
||||
}
|
||||
|
||||
addTemplate (template) {
|
||||
if (!template) {
|
||||
throw new Error('Invalid template: ' + JSON.stringify(template))
|
||||
}
|
||||
|
||||
// Validate & parse source
|
||||
const src = template.src || template
|
||||
const srcPath = path.parse(src)
|
||||
|
||||
if (typeof src !== 'string' || !fs.existsSync(src)) {
|
||||
throw new Error('Template src not found: ' + src)
|
||||
}
|
||||
|
||||
// Mostly for DX, some people prefers `filename` vs `fileName`
|
||||
const fileName = template.fileName || template.filename
|
||||
// Generate unique and human readable dst filename if not provided
|
||||
const dst = fileName || `${path.basename(srcPath.dir)}.${srcPath.name}.${hash(src)}${srcPath.ext}`
|
||||
// Add to templates list
|
||||
const templateObj = {
|
||||
src,
|
||||
dst,
|
||||
options: template.options
|
||||
}
|
||||
|
||||
this.options.build.templates.push(templateObj)
|
||||
|
||||
return templateObj
|
||||
}
|
||||
|
||||
addPlugin (template) {
|
||||
const { dst } = this.addTemplate(template)
|
||||
|
||||
// Add to nuxt plugins
|
||||
this.options.plugins.unshift({
|
||||
src: path.join(this.options.buildDir, dst),
|
||||
// TODO: remove deprecated option in Nuxt 3
|
||||
ssr: template.ssr,
|
||||
mode: template.mode
|
||||
})
|
||||
}
|
||||
|
||||
addLayout (template, name) {
|
||||
const { dst, src } = this.addTemplate(template)
|
||||
const layoutName = name || path.parse(src).name
|
||||
const layout = this.options.layouts[layoutName]
|
||||
|
||||
if (layout) {
|
||||
consola.warn(`Duplicate layout registration, "${layoutName}" has been registered as "${layout}"`)
|
||||
}
|
||||
|
||||
// Add to nuxt layouts
|
||||
this.options.layouts[layoutName] = `./${dst}`
|
||||
|
||||
// If error layout, set ErrorPage
|
||||
if (name === 'error') {
|
||||
this.addErrorLayout(dst)
|
||||
}
|
||||
}
|
||||
|
||||
addErrorLayout (dst) {
|
||||
const relativeBuildDir = path.relative(this.options.rootDir, this.options.buildDir)
|
||||
this.options.ErrorPage = `~/${relativeBuildDir}/${dst}`
|
||||
}
|
||||
|
||||
addServerMiddleware (middleware) {
|
||||
this.options.serverMiddleware.push(middleware)
|
||||
}
|
||||
|
||||
extendBuild (fn) {
|
||||
this.options.build.extend = chainFn(this.options.build.extend, fn)
|
||||
}
|
||||
|
||||
extendRoutes (fn) {
|
||||
this.options.router.extendRoutes = chainFn(
|
||||
this.options.router.extendRoutes,
|
||||
fn
|
||||
)
|
||||
}
|
||||
|
||||
requireModule (moduleOpts) {
|
||||
return this.addModule(moduleOpts)
|
||||
}
|
||||
|
||||
async addModule (moduleOpts) {
|
||||
let src
|
||||
let options
|
||||
let handler
|
||||
|
||||
// Type 1: String or Function
|
||||
if (typeof moduleOpts === 'string' || typeof moduleOpts === 'function') {
|
||||
src = moduleOpts
|
||||
} else if (Array.isArray(moduleOpts)) {
|
||||
// Type 2: Babel style array
|
||||
[src, options] = moduleOpts
|
||||
} else if (typeof moduleOpts === 'object') {
|
||||
// Type 3: Pure object
|
||||
({ src, options, handler } = moduleOpts)
|
||||
}
|
||||
|
||||
// Define handler if src is a function
|
||||
if (typeof src === 'function') {
|
||||
handler = src
|
||||
}
|
||||
|
||||
// Prevent adding buildModules-listed entries in production
|
||||
if (this.options.buildModules.includes(handler) && this.options._start) {
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve handler
|
||||
if (!handler) {
|
||||
try {
|
||||
handler = this.nuxt.resolver.requireModule(src, { useESM: true })
|
||||
} catch (error) {
|
||||
if (error.code !== 'MODULE_NOT_FOUND') {
|
||||
throw error
|
||||
}
|
||||
|
||||
// Hint only if entrypoint is not found and src is not local alias or path
|
||||
if (error.message.includes(src) && !/^[~.]|^@\//.test(src)) {
|
||||
let message = 'Module `{name}` not found.'
|
||||
|
||||
if (this.options.buildModules.includes(src)) {
|
||||
message += ' Please ensure `{name}` is in `devDependencies` and installed. HINT: During build step, for npm/yarn, `NODE_ENV=production` or `--production` should NOT be used.'.replace('{name}', src)
|
||||
} else if (this.options.modules.includes(src)) {
|
||||
message += ' Please ensure `{name}` is in `dependencies` and installed.'
|
||||
}
|
||||
|
||||
message = message.replace(/{name}/g, src)
|
||||
|
||||
consola.warn(message)
|
||||
}
|
||||
|
||||
if (this.options._cli) {
|
||||
throw error
|
||||
} else {
|
||||
// TODO: Remove in next major version
|
||||
consola.warn('Silently ignoring module as programatic usage detected.')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate handler
|
||||
if (typeof handler !== 'function') {
|
||||
throw new TypeError('Module should export a function: ' + src)
|
||||
}
|
||||
|
||||
// Ensure module is required once
|
||||
const metaKey = handler.meta && handler.meta.name
|
||||
const key = metaKey || src
|
||||
if (typeof key === 'string') {
|
||||
if (this.requiredModules[key]) {
|
||||
if (!metaKey) {
|
||||
// TODO: Skip with nuxt3
|
||||
consola.warn('Modules should be only specified once:', key)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.requiredModules[key] = { src, options, handler }
|
||||
}
|
||||
|
||||
// Default module options to empty object
|
||||
if (options === undefined) {
|
||||
options = {}
|
||||
}
|
||||
const result = await handler.call(this, options)
|
||||
return result
|
||||
}
|
||||
}
|
115
packages/nuxt3/src/core/nuxt.ts
Normal file
115
packages/nuxt3/src/core/nuxt.ts
Normal file
@ -0,0 +1,115 @@
|
||||
|
||||
import isPlainObject from 'lodash/isPlainObject'
|
||||
import consola from 'consola'
|
||||
import Hookable from 'hable'
|
||||
|
||||
import { defineAlias } from 'src/utils'
|
||||
import { getNuxtConfig } from 'src/config'
|
||||
import { Server } from 'src/server'
|
||||
|
||||
import { version } from '../../package.json'
|
||||
|
||||
import ModuleContainer from './module'
|
||||
import Resolver from './resolver'
|
||||
|
||||
export default class Nuxt extends Hookable {
|
||||
constructor (options = {}) {
|
||||
super(consola)
|
||||
|
||||
// Assign options and apply defaults
|
||||
this.options = getNuxtConfig(options)
|
||||
|
||||
// Create instance of core components
|
||||
this.resolver = new Resolver(this)
|
||||
this.moduleContainer = new ModuleContainer(this)
|
||||
|
||||
// Deprecated hooks
|
||||
this.deprecateHooks({
|
||||
// #3294 - 7514db73b25c23b8c14ebdafbb4e129ac282aabd
|
||||
'render:context': {
|
||||
to: '_render:context',
|
||||
message: '`render:context(nuxt)` is deprecated, Please use `vue-renderer:ssr:context(context)`'
|
||||
},
|
||||
// #3773
|
||||
'render:routeContext': {
|
||||
to: '_render:context',
|
||||
message: '`render:routeContext(nuxt)` is deprecated, Please use `vue-renderer:ssr:context(context)`'
|
||||
},
|
||||
showReady: 'webpack:done'
|
||||
})
|
||||
|
||||
// Add Legacy aliases
|
||||
defineAlias(this, this.resolver, ['resolveAlias', 'resolvePath'])
|
||||
this.showReady = () => { this.callHook('webpack:done') }
|
||||
|
||||
// Init server
|
||||
if (this.options.server !== false) {
|
||||
this._initServer()
|
||||
}
|
||||
|
||||
// Call ready
|
||||
if (this.options._ready !== false) {
|
||||
this.ready().catch((err) => {
|
||||
consola.fatal(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static get version () {
|
||||
return `v${version}` + (global.__NUXT_DEV__ ? '-development' : '')
|
||||
}
|
||||
|
||||
ready () {
|
||||
if (!this._ready) {
|
||||
this._ready = this._init()
|
||||
}
|
||||
return this._ready
|
||||
}
|
||||
|
||||
async _init () {
|
||||
if (this._initCalled) {
|
||||
return this
|
||||
}
|
||||
this._initCalled = true
|
||||
|
||||
// Add hooks
|
||||
if (isPlainObject(this.options.hooks)) {
|
||||
this.addHooks(this.options.hooks)
|
||||
} else if (typeof this.options.hooks === 'function') {
|
||||
this.options.hooks(this.hook)
|
||||
}
|
||||
|
||||
// Await for modules
|
||||
await this.moduleContainer.ready()
|
||||
|
||||
// Await for server to be ready
|
||||
if (this.server) {
|
||||
await this.server.ready()
|
||||
}
|
||||
|
||||
// Call ready hook
|
||||
await this.callHook('ready', this)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
_initServer () {
|
||||
if (this.server) {
|
||||
return
|
||||
}
|
||||
this.server = new Server(this)
|
||||
this.renderer = this.server
|
||||
this.render = this.server.app
|
||||
defineAlias(this, this.server, ['renderRoute', 'renderAndGetWindow', 'listen'])
|
||||
}
|
||||
|
||||
async close (callback) {
|
||||
await this.callHook('close', this)
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
await callback()
|
||||
}
|
||||
|
||||
this.clearHooks()
|
||||
}
|
||||
}
|
182
packages/nuxt3/src/core/resolver.ts
Normal file
182
packages/nuxt3/src/core/resolver.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import { resolve, join } from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import consola from 'consola'
|
||||
|
||||
import {
|
||||
startsWithRootAlias,
|
||||
startsWithSrcAlias,
|
||||
isExternalDependency,
|
||||
clearRequireCache
|
||||
} from 'src/utils'
|
||||
|
||||
export default class Resolver {
|
||||
constructor (nuxt) {
|
||||
this.nuxt = nuxt
|
||||
this.options = this.nuxt.options
|
||||
|
||||
// Binds
|
||||
this.resolvePath = this.resolvePath.bind(this)
|
||||
this.resolveAlias = this.resolveAlias.bind(this)
|
||||
this.resolveModule = this.resolveModule.bind(this)
|
||||
this.requireModule = this.requireModule.bind(this)
|
||||
|
||||
const { createRequire } = this.options
|
||||
this._require = createRequire ? createRequire(module) : module.require
|
||||
|
||||
this._resolve = require.resolve
|
||||
}
|
||||
|
||||
resolveModule (path) {
|
||||
try {
|
||||
return this._resolve(path, {
|
||||
paths: this.options.modulesDir
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.code !== 'MODULE_NOT_FOUND') {
|
||||
// TODO: remove after https://github.com/facebook/jest/pull/8487 released
|
||||
if (process.env.NODE_ENV === 'test' && error.message.startsWith('Cannot resolve module')) {
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolveAlias (path) {
|
||||
if (startsWithRootAlias(path)) {
|
||||
return join(this.options.rootDir, path.substr(2))
|
||||
}
|
||||
|
||||
if (startsWithSrcAlias(path)) {
|
||||
return join(this.options.srcDir, path.substr(1))
|
||||
}
|
||||
|
||||
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.')
|
||||
}
|
||||
|
||||
// Fast return in case of path exists
|
||||
if (fs.existsSync(path)) {
|
||||
return path
|
||||
}
|
||||
|
||||
let resolvedPath
|
||||
|
||||
// Try to resolve it as a regular module
|
||||
if (isModule !== false) {
|
||||
resolvedPath = this.resolveModule(path)
|
||||
}
|
||||
|
||||
// Try to resolve alias
|
||||
if (!resolvedPath && isAlias !== false) {
|
||||
resolvedPath = this.resolveAlias(path)
|
||||
}
|
||||
|
||||
// Use path for resolvedPath
|
||||
if (!resolvedPath) {
|
||||
resolvedPath = path
|
||||
}
|
||||
|
||||
let isDirectory
|
||||
|
||||
// Check if resolvedPath exits and is not a directory
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
isDirectory = fs.lstatSync(resolvedPath).isDirectory()
|
||||
|
||||
if (!isDirectory) {
|
||||
return resolvedPath
|
||||
}
|
||||
}
|
||||
|
||||
const extensions = isStyle ? this.options.styleExtensions : this.options.extensions
|
||||
|
||||
// Check if any resolvedPath.[ext] or resolvedPath/index.[ext] exists
|
||||
for (const ext of extensions) {
|
||||
if (!isDirectory && fs.existsSync(resolvedPath + '.' + ext)) {
|
||||
return resolvedPath + '.' + ext
|
||||
}
|
||||
|
||||
const resolvedPathwithIndex = join(resolvedPath, 'index.' + ext)
|
||||
if (isDirectory && fs.existsSync(resolvedPathwithIndex)) {
|
||||
return resolvedPathwithIndex
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no index.[ext] we just return the directory path
|
||||
if (isDirectory) {
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
// Give up
|
||||
throw new Error(`Cannot resolve "${path}" from "${resolvedPath}"`)
|
||||
}
|
||||
|
||||
requireModule (path, { esm, useESM = esm, alias, isAlias = alias, intropDefault, interopDefault = intropDefault } = {}) {
|
||||
let resolvedPath = path
|
||||
let requiredModule
|
||||
|
||||
// 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
|
||||
|
||||
// Try to resolve path
|
||||
try {
|
||||
resolvedPath = this.resolvePath(path, { isAlias })
|
||||
} catch (e) {
|
||||
lastError = e
|
||||
}
|
||||
|
||||
const isExternal = isExternalDependency(resolvedPath)
|
||||
|
||||
// in dev mode make sure to clear the require cache so after
|
||||
// a dev server restart any changed file is reloaded
|
||||
if (this.options.dev && !isExternal) {
|
||||
clearRequireCache(resolvedPath)
|
||||
}
|
||||
|
||||
// By default use esm only for js,mjs files outside of node_modules
|
||||
if (useESM === undefined) {
|
||||
useESM = !isExternal && /.(js|mjs)$/.test(resolvedPath)
|
||||
}
|
||||
|
||||
// Try to require
|
||||
try {
|
||||
if (useESM) {
|
||||
requiredModule = this._require(resolvedPath)
|
||||
} else {
|
||||
requiredModule = require(resolvedPath)
|
||||
}
|
||||
} catch (e) {
|
||||
lastError = e
|
||||
}
|
||||
|
||||
// Interop default
|
||||
if (interopDefault !== false && requiredModule && requiredModule.default) {
|
||||
requiredModule = requiredModule.default
|
||||
}
|
||||
|
||||
// Throw error if failed to require
|
||||
if (requiredModule === undefined && lastError) {
|
||||
throw lastError
|
||||
}
|
||||
|
||||
return requiredModule
|
||||
}
|
||||
}
|
412
packages/nuxt3/src/generator/generator.ts
Normal file
412
packages/nuxt3/src/generator/generator.ts
Normal file
@ -0,0 +1,412 @@
|
||||
import path from 'path'
|
||||
import chalk from 'chalk'
|
||||
import consola from 'consola'
|
||||
import fsExtra from 'fs-extra'
|
||||
import defu from 'defu'
|
||||
import htmlMinifier from 'html-minifier'
|
||||
import { parse } from 'node-html-parser'
|
||||
|
||||
import { isFullStatic, flatRoutes, isString, isUrl, promisifyRoute, waitFor, TARGETS } from 'src/utils'
|
||||
|
||||
export default class Generator {
|
||||
constructor (nuxt, builder) {
|
||||
this.nuxt = nuxt
|
||||
this.options = nuxt.options
|
||||
this.builder = builder
|
||||
this.isFullStatic = false
|
||||
|
||||
// Set variables
|
||||
this.staticRoutes = path.resolve(this.options.srcDir, this.options.dir.static)
|
||||
this.srcBuiltPath = path.resolve(this.options.buildDir, 'dist', 'client')
|
||||
this.distPath = this.options.generate.dir
|
||||
this.distNuxtPath = path.join(
|
||||
this.distPath,
|
||||
isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath
|
||||
)
|
||||
|
||||
// Shared payload
|
||||
this._payload = null
|
||||
this.setPayload = (payload) => {
|
||||
this._payload = defu(payload, this._payload)
|
||||
}
|
||||
}
|
||||
|
||||
async generate ({ build = true, init = true } = {}) {
|
||||
consola.debug('Initializing generator...')
|
||||
await this.initiate({ build, init })
|
||||
|
||||
// Payloads for full static
|
||||
if (this.isFullStatic) {
|
||||
consola.info('Full static mode activated')
|
||||
const { staticAssets } = this.options.generate
|
||||
this.staticAssetsDir = path.resolve(this.distNuxtPath, staticAssets.dir, staticAssets.version)
|
||||
this.staticAssetsBase = this.options.generate.staticAssets.versionBase
|
||||
}
|
||||
|
||||
consola.debug('Preparing routes for generate...')
|
||||
const routes = await this.initRoutes()
|
||||
|
||||
consola.info('Generating pages')
|
||||
const errors = await this.generateRoutes(routes)
|
||||
|
||||
await this.afterGenerate()
|
||||
|
||||
// Done hook
|
||||
await this.nuxt.callHook('generate:done', this, errors)
|
||||
await this.nuxt.callHook('export:done', this, { errors })
|
||||
|
||||
return { errors }
|
||||
}
|
||||
|
||||
async initiate ({ build = true, init = true } = {}) {
|
||||
// Wait for nuxt be ready
|
||||
await this.nuxt.ready()
|
||||
|
||||
// Call before hook
|
||||
await this.nuxt.callHook('generate:before', this, this.options.generate)
|
||||
await this.nuxt.callHook('export:before', this)
|
||||
|
||||
if (build) {
|
||||
// Add flag to set process.static
|
||||
this.builder.forGenerate()
|
||||
|
||||
// Start build process
|
||||
await this.builder.build()
|
||||
this.isFullStatic = isFullStatic(this.options)
|
||||
} else {
|
||||
const hasBuilt = await fsExtra.exists(path.resolve(this.options.buildDir, 'dist', 'server', 'client.manifest.json'))
|
||||
if (!hasBuilt) {
|
||||
const fullStaticArgs = isFullStatic(this.options) ? ' --target static' : ''
|
||||
throw new Error(
|
||||
`No build files found in ${this.srcBuiltPath}.\nPlease run \`nuxt build${fullStaticArgs}\` before calling \`nuxt export\``
|
||||
)
|
||||
}
|
||||
const config = this.getBuildConfig()
|
||||
if (!config || (config.target !== TARGETS.static && !this.options._legacyGenerate)) {
|
||||
throw new Error(
|
||||
'In order to use `nuxt export`, you need to run `nuxt build --target static`'
|
||||
)
|
||||
}
|
||||
this.isFullStatic = config.isFullStatic
|
||||
this.options.render.ssr = config.ssr
|
||||
}
|
||||
|
||||
// Initialize dist directory
|
||||
if (init) {
|
||||
await this.initDist()
|
||||
}
|
||||
}
|
||||
|
||||
async initRoutes (...args) {
|
||||
// Resolve config.generate.routes promises before generating the routes
|
||||
let generateRoutes = []
|
||||
if (this.options.router.mode !== 'hash') {
|
||||
try {
|
||||
generateRoutes = await promisifyRoute(
|
||||
this.options.generate.routes || [],
|
||||
...args
|
||||
)
|
||||
} catch (e) {
|
||||
consola.error('Could not resolve routes')
|
||||
throw e // eslint-disable-line no-unreachable
|
||||
}
|
||||
}
|
||||
let routes = []
|
||||
// Generate only index.html for router.mode = 'hash' or client-side apps
|
||||
if (this.options.router.mode === 'hash') {
|
||||
routes = ['/']
|
||||
} else {
|
||||
routes = flatRoutes(this.getAppRoutes())
|
||||
}
|
||||
routes = routes.filter(route => this.shouldGenerateRoute(route))
|
||||
routes = this.decorateWithPayloads(routes, generateRoutes)
|
||||
|
||||
// extendRoutes hook
|
||||
await this.nuxt.callHook('generate:extendRoutes', routes)
|
||||
await this.nuxt.callHook('export:extendRoutes', { routes })
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
shouldGenerateRoute (route) {
|
||||
return this.options.generate.exclude.every((regex) => {
|
||||
if (typeof regex === 'string') {
|
||||
return regex !== route
|
||||
}
|
||||
return !regex.test(route)
|
||||
})
|
||||
}
|
||||
|
||||
getBuildConfig () {
|
||||
try {
|
||||
return require(path.join(this.options.buildDir, 'nuxt/config.json'))
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
getAppRoutes () {
|
||||
return require(path.join(this.options.buildDir, 'routes.json'))
|
||||
}
|
||||
|
||||
async generateRoutes (routes) {
|
||||
const errors = []
|
||||
|
||||
this.routes = []
|
||||
this.generatedRoutes = new Set()
|
||||
|
||||
routes.forEach(({ route, ...props }) => {
|
||||
route = decodeURI(route)
|
||||
this.routes.push({ route, ...props })
|
||||
// Add routes to the tracked generated routes (for crawler)
|
||||
this.generatedRoutes.add(route)
|
||||
})
|
||||
|
||||
// Start generate process
|
||||
while (this.routes.length) {
|
||||
let n = 0
|
||||
await Promise.all(
|
||||
this.routes
|
||||
.splice(0, this.options.generate.concurrency)
|
||||
.map(async ({ route, payload }) => {
|
||||
await waitFor(n++ * this.options.generate.interval)
|
||||
await this.generateRoute({ route, payload, errors })
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Improve string representation for errors
|
||||
// TODO: Use consola for more consistency
|
||||
errors.toString = () => this._formatErrors(errors)
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
_formatErrors (errors) {
|
||||
return errors
|
||||
.map(({ type, route, error }) => {
|
||||
const isHandled = type === 'handled'
|
||||
const color = isHandled ? 'yellow' : 'red'
|
||||
|
||||
let line = chalk[color](` ${route}\n\n`)
|
||||
|
||||
if (isHandled) {
|
||||
line += chalk.grey(JSON.stringify(error, undefined, 2) + '\n')
|
||||
} else {
|
||||
line += chalk.grey(error.stack || error.message || `${error}`)
|
||||
}
|
||||
|
||||
return line
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
async afterGenerate () {
|
||||
const { fallback } = this.options.generate
|
||||
|
||||
// Disable SPA fallback if value isn't a non-empty string
|
||||
if (typeof fallback !== 'string' || !fallback) {
|
||||
return
|
||||
}
|
||||
|
||||
const fallbackPath = path.join(this.distPath, fallback)
|
||||
|
||||
// Prevent conflicts
|
||||
if (await fsExtra.exists(fallbackPath)) {
|
||||
consola.warn(`SPA fallback was configured, but the configured path (${fallbackPath}) already exists.`)
|
||||
return
|
||||
}
|
||||
|
||||
// Render and write the SPA template to the fallback path
|
||||
let { html } = await this.nuxt.server.renderRoute('/', {
|
||||
spa: true,
|
||||
staticAssetsBase: this.staticAssetsBase
|
||||
})
|
||||
|
||||
try {
|
||||
html = this.minifyHtml(html)
|
||||
} catch (error) {
|
||||
consola.warn('HTML minification failed for SPA fallback')
|
||||
}
|
||||
|
||||
await fsExtra.writeFile(fallbackPath, html, 'utf8')
|
||||
consola.success('Client-side fallback created: `' + fallback + '`')
|
||||
}
|
||||
|
||||
async initDist () {
|
||||
// Clean destination folder
|
||||
await fsExtra.emptyDir(this.distPath)
|
||||
|
||||
consola.info(`Generating output directory: ${path.basename(this.distPath)}/`)
|
||||
await this.nuxt.callHook('generate:distRemoved', this)
|
||||
await this.nuxt.callHook('export:distRemoved', this)
|
||||
|
||||
// Copy static and built files
|
||||
if (await fsExtra.exists(this.staticRoutes)) {
|
||||
await fsExtra.copy(this.staticRoutes, this.distPath)
|
||||
}
|
||||
// Copy .nuxt/dist/client/ to dist/_nuxt/
|
||||
await fsExtra.copy(this.srcBuiltPath, this.distNuxtPath)
|
||||
|
||||
if (this.payloadDir) {
|
||||
await fsExtra.ensureDir(this.payloadDir)
|
||||
}
|
||||
|
||||
// Add .nojekyll file to let GitHub Pages add the _nuxt/ folder
|
||||
// https://help.github.com/articles/files-that-start-with-an-underscore-are-missing/
|
||||
const nojekyllPath = path.resolve(this.distPath, '.nojekyll')
|
||||
fsExtra.writeFile(nojekyllPath, '')
|
||||
|
||||
await this.nuxt.callHook('generate:distCopied', this)
|
||||
await this.nuxt.callHook('export:distCopied', this)
|
||||
}
|
||||
|
||||
decorateWithPayloads (routes, generateRoutes) {
|
||||
const routeMap = {}
|
||||
// Fill routeMap for known routes
|
||||
routes.forEach((route) => {
|
||||
routeMap[route] = { route, payload: null }
|
||||
})
|
||||
// Fill routeMap with given generate.routes
|
||||
generateRoutes.forEach((route) => {
|
||||
// route is either a string or like { route : '/my_route/1', payload: {} }
|
||||
const path = isString(route) ? route : route.route
|
||||
routeMap[path] = {
|
||||
route: path,
|
||||
payload: route.payload || null
|
||||
}
|
||||
})
|
||||
return Object.values(routeMap)
|
||||
}
|
||||
|
||||
async generateRoute ({ route, payload = {}, errors = [] }) {
|
||||
let html
|
||||
const pageErrors = []
|
||||
|
||||
const setPayload = (_payload) => {
|
||||
payload = defu(_payload, payload)
|
||||
}
|
||||
|
||||
// Apply shared payload
|
||||
if (this._payload) {
|
||||
payload = defu(payload, this._payload)
|
||||
}
|
||||
|
||||
await this.nuxt.callHook('generate:route', { route, setPayload })
|
||||
await this.nuxt.callHook('export:route', { route, setPayload })
|
||||
|
||||
try {
|
||||
const renderContext = {
|
||||
payload,
|
||||
staticAssetsBase: this.staticAssetsBase
|
||||
}
|
||||
const res = await this.nuxt.server.renderRoute(route, renderContext)
|
||||
html = res.html
|
||||
|
||||
// If crawler activated and called from generateRoutes()
|
||||
if (this.options.generate.crawler && this.options.render.ssr) {
|
||||
const possibleTrailingSlash = this.options.router.trailingSlash ? '/' : ''
|
||||
parse(html).querySelectorAll('a').map((el) => {
|
||||
const sanitizedHref = (el.getAttribute('href') || '')
|
||||
.replace(this.options.router.base, '/')
|
||||
.replace(/\/+$/, '')
|
||||
.split('?')[0]
|
||||
.split('#')[0]
|
||||
.trim()
|
||||
|
||||
const route = decodeURI(sanitizedHref + possibleTrailingSlash)
|
||||
|
||||
if (route.startsWith('/') && !path.extname(route) && this.shouldGenerateRoute(route) && !this.generatedRoutes.has(route)) {
|
||||
this.generatedRoutes.add(route)
|
||||
this.routes.push({ route })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Save Static Assets
|
||||
if (this.staticAssetsDir && renderContext.staticAssets) {
|
||||
for (const asset of renderContext.staticAssets) {
|
||||
const assetPath = path.join(this.staticAssetsDir, asset.path)
|
||||
await fsExtra.ensureDir(path.dirname(assetPath))
|
||||
await fsExtra.writeFile(assetPath, asset.src, 'utf-8')
|
||||
}
|
||||
}
|
||||
|
||||
if (res.error) {
|
||||
pageErrors.push({ type: 'handled', route, error: res.error })
|
||||
}
|
||||
} catch (err) {
|
||||
pageErrors.push({ type: 'unhandled', route, error: err })
|
||||
errors.push(...pageErrors)
|
||||
|
||||
await this.nuxt.callHook('generate:routeFailed', { route, errors: pageErrors })
|
||||
await this.nuxt.callHook('export:routeFailed', { route, errors: pageErrors })
|
||||
consola.error(this._formatErrors(pageErrors))
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
html = this.minifyHtml(html)
|
||||
} catch (err) {
|
||||
const minifyErr = new Error(
|
||||
`HTML minification failed. Make sure the route generates valid HTML. Failed HTML:\n ${html}`
|
||||
)
|
||||
pageErrors.push({ type: 'unhandled', route, error: minifyErr })
|
||||
}
|
||||
|
||||
let fileName
|
||||
|
||||
if (this.options.generate.subFolders) {
|
||||
fileName = path.join(route, path.sep, 'index.html') // /about -> /about/index.html
|
||||
fileName = fileName === '/404/index.html' ? '/404.html' : fileName // /404 -> /404.html
|
||||
} else {
|
||||
const normalizedRoute = route.replace(/\/$/, '')
|
||||
fileName = route.length > 1 ? path.join(path.sep, normalizedRoute + '.html') : path.join(path.sep, 'index.html')
|
||||
}
|
||||
|
||||
// Call hook to let user update the path & html
|
||||
const page = { route, path: fileName, html, exclude: false }
|
||||
await this.nuxt.callHook('generate:page', page)
|
||||
await this.nuxt.callHook('export:page', { page, errors: pageErrors })
|
||||
|
||||
if (page.exclude) {
|
||||
return false
|
||||
}
|
||||
page.path = path.join(this.distPath, page.path)
|
||||
|
||||
// Make sure the sub folders are created
|
||||
await fsExtra.mkdirp(path.dirname(page.path))
|
||||
await fsExtra.writeFile(page.path, page.html, 'utf8')
|
||||
|
||||
await this.nuxt.callHook('generate:routeCreated', { route, path: page.path, errors: pageErrors })
|
||||
await this.nuxt.callHook('export:routeCreated', { route, path: page.path, errors: pageErrors })
|
||||
|
||||
if (pageErrors.length) {
|
||||
consola.error(`Error generating route "${route}": ${pageErrors.map(e => e.error.message).join(', ')}`)
|
||||
errors.push(...pageErrors)
|
||||
} else {
|
||||
consola.success(`Generated route "${route}"`)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
minifyHtml (html) {
|
||||
let minificationOptions = this.options.build.html.minify
|
||||
|
||||
// Legacy: Override minification options with generate.minify if present
|
||||
// TODO: Remove in Nuxt version 3
|
||||
if (typeof this.options.generate.minify !== 'undefined') {
|
||||
minificationOptions = this.options.generate.minify
|
||||
consola.warn('generate.minify has been deprecated and will be removed in the next major version.' +
|
||||
' Use build.html.minify instead!')
|
||||
}
|
||||
|
||||
if (!minificationOptions) {
|
||||
return html
|
||||
}
|
||||
|
||||
return htmlMinifier.minify(html, minificationOptions)
|
||||
}
|
||||
}
|
6
packages/nuxt3/src/generator/index.ts
Normal file
6
packages/nuxt3/src/generator/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import Generator from './generator'
|
||||
export { default as Generator } from './generator'
|
||||
|
||||
export function getGenerator (nuxt) {
|
||||
return new Generator(nuxt)
|
||||
}
|
8
packages/nuxt3/src/server/context.ts
Normal file
8
packages/nuxt3/src/server/context.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export default class ServerContext {
|
||||
constructor (server) {
|
||||
this.nuxt = server.nuxt
|
||||
this.globals = server.globals
|
||||
this.options = server.options
|
||||
this.resources = server.resources
|
||||
}
|
||||
}
|
2
packages/nuxt3/src/server/index.ts
Normal file
2
packages/nuxt3/src/server/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as Server } from './server'
|
||||
export { default as Listener } from './listener'
|
72
packages/nuxt3/src/server/jsdom.ts
Normal file
72
packages/nuxt3/src/server/jsdom.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import consola from 'consola'
|
||||
import { timeout } from 'src/utils'
|
||||
|
||||
export default async function renderAndGetWindow (
|
||||
url = 'http://localhost:3000',
|
||||
jsdomOpts = {},
|
||||
{
|
||||
loadedCallback,
|
||||
loadingTimeout = 2000,
|
||||
globals
|
||||
} = {}
|
||||
) {
|
||||
const jsdom = await import('jsdom')
|
||||
.then(m => m.default || m)
|
||||
.catch((e) => {
|
||||
consola.error(`
|
||||
jsdom is not installed. Please install jsdom with:
|
||||
$ yarn add --dev jsdom
|
||||
OR
|
||||
$ npm install --dev jsdom
|
||||
`)
|
||||
throw e
|
||||
})
|
||||
|
||||
const options = Object.assign({
|
||||
// Load subresources (https://github.com/tmpvar/jsdom#loading-subresources)
|
||||
resources: 'usable',
|
||||
runScripts: 'dangerously',
|
||||
virtualConsole: true,
|
||||
beforeParse (window) {
|
||||
// Mock window.scrollTo
|
||||
window.scrollTo = () => {}
|
||||
}
|
||||
}, jsdomOpts)
|
||||
|
||||
const jsdomErrHandler = (err) => {
|
||||
throw err
|
||||
}
|
||||
|
||||
if (options.virtualConsole) {
|
||||
if (options.virtualConsole === true) {
|
||||
options.virtualConsole = new jsdom.VirtualConsole().sendTo(consola)
|
||||
}
|
||||
// Throw error when window creation failed
|
||||
options.virtualConsole.on('jsdomError', jsdomErrHandler)
|
||||
}
|
||||
|
||||
const { window } = await jsdom.JSDOM.fromURL(url, options)
|
||||
|
||||
// If Nuxt could not be loaded (error from the server-side)
|
||||
const nuxtExists = window.document.body.innerHTML.includes(`id="${globals.id}"`)
|
||||
|
||||
if (!nuxtExists) {
|
||||
const error = new Error('Could not load the nuxt app')
|
||||
error.body = window.document.body.innerHTML
|
||||
window.close()
|
||||
throw error
|
||||
}
|
||||
|
||||
// Used by Nuxt.js to say when the components are loaded and the app ready
|
||||
await timeout(new Promise((resolve) => {
|
||||
window[loadedCallback] = () => resolve(window)
|
||||
}), loadingTimeout, `Components loading in renderAndGetWindow was not completed in ${loadingTimeout / 1000}s`)
|
||||
|
||||
if (options.virtualConsole) {
|
||||
// After window initialized successfully
|
||||
options.virtualConsole.removeListener('jsdomError', jsdomErrHandler)
|
||||
}
|
||||
|
||||
// Send back window object
|
||||
return window
|
||||
}
|
119
packages/nuxt3/src/server/listener.ts
Normal file
119
packages/nuxt3/src/server/listener.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import http from 'http'
|
||||
import https from 'https'
|
||||
import enableDestroy from 'server-destroy'
|
||||
import ip from 'ip'
|
||||
import consola from 'consola'
|
||||
import pify from 'pify'
|
||||
|
||||
let RANDOM_PORT = '0'
|
||||
|
||||
export default class Listener {
|
||||
constructor ({ port, host, socket, https, app, dev, baseURL }) {
|
||||
// Options
|
||||
this.port = port
|
||||
this.host = host
|
||||
this.socket = socket
|
||||
this.https = https
|
||||
this.app = app
|
||||
this.dev = dev
|
||||
this.baseURL = baseURL
|
||||
|
||||
// After listen
|
||||
this.listening = false
|
||||
this._server = null
|
||||
this.server = null
|
||||
this.address = null
|
||||
this.url = null
|
||||
}
|
||||
|
||||
async close () {
|
||||
// Destroy server by forcing every connection to be closed
|
||||
if (this.server && this.server.listening) {
|
||||
await this.server.destroy()
|
||||
consola.debug('server closed')
|
||||
}
|
||||
|
||||
// Delete references
|
||||
this.listening = false
|
||||
this._server = null
|
||||
this.server = null
|
||||
this.address = null
|
||||
this.url = null
|
||||
}
|
||||
|
||||
computeURL () {
|
||||
const address = this.server.address()
|
||||
if (!this.socket) {
|
||||
switch (address.address) {
|
||||
case '127.0.0.1': this.host = 'localhost'; break
|
||||
case '0.0.0.0': this.host = ip.address(); break
|
||||
}
|
||||
this.port = address.port
|
||||
this.url = `http${this.https ? 's' : ''}://${this.host}:${this.port}${this.baseURL}`
|
||||
return
|
||||
}
|
||||
this.url = `unix+http://${address}`
|
||||
}
|
||||
|
||||
async listen () {
|
||||
// Prevent multi calls
|
||||
if (this.listening) {
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize underlying http(s) server
|
||||
const protocol = this.https ? https : http
|
||||
const protocolOpts = this.https ? [this.https] : []
|
||||
this._server = protocol.createServer.apply(protocol, protocolOpts.concat(this.app))
|
||||
|
||||
// Call server.listen
|
||||
// Prepare listenArgs
|
||||
const listenArgs = this.socket ? { path: this.socket } : { host: this.host, port: this.port }
|
||||
listenArgs.exclusive = false
|
||||
|
||||
// Call server.listen
|
||||
try {
|
||||
this.server = await new Promise((resolve, reject) => {
|
||||
this._server.on('error', error => reject(error))
|
||||
const s = this._server.listen(listenArgs, error => error ? reject(error) : resolve(s))
|
||||
})
|
||||
} catch (error) {
|
||||
return this.serverErrorHandler(error)
|
||||
}
|
||||
|
||||
// Enable destroy support
|
||||
enableDestroy(this.server)
|
||||
pify(this.server.destroy)
|
||||
|
||||
// Compute listen URL
|
||||
this.computeURL()
|
||||
|
||||
// Set this.listening to true
|
||||
this.listening = true
|
||||
}
|
||||
|
||||
async serverErrorHandler (error) {
|
||||
// Detect if port is not available
|
||||
const addressInUse = error.code === 'EADDRINUSE'
|
||||
|
||||
// Use better error message
|
||||
if (addressInUse) {
|
||||
const address = this.socket || `${this.host}:${this.port}`
|
||||
error.message = `Address \`${address}\` is already in use.`
|
||||
|
||||
// Listen to a random port on dev as a fallback
|
||||
if (this.dev && !this.socket && this.port !== RANDOM_PORT) {
|
||||
consola.warn(error.message)
|
||||
consola.info('Trying a random port...')
|
||||
this.port = RANDOM_PORT
|
||||
await this.close()
|
||||
await this.listen()
|
||||
RANDOM_PORT = this.port
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Throw error
|
||||
throw error
|
||||
}
|
||||
}
|
130
packages/nuxt3/src/server/middleware/error.ts
Normal file
130
packages/nuxt3/src/server/middleware/error.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import consola from 'consola'
|
||||
|
||||
import Youch from '@nuxtjs/youch'
|
||||
|
||||
export default ({ resources, options }) => async function errorMiddleware (_error, req, res, next) {
|
||||
// Normalize error
|
||||
const error = normalizeError(_error, options)
|
||||
|
||||
const sendResponse = (content, type = 'text/html') => {
|
||||
// Set Headers
|
||||
res.statusCode = error.statusCode
|
||||
res.statusMessage = 'RuntimeError'
|
||||
res.setHeader('Content-Type', type + '; charset=utf-8')
|
||||
res.setHeader('Content-Length', Buffer.byteLength(content))
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate')
|
||||
|
||||
// Error headers
|
||||
if (error.headers) {
|
||||
for (const name in error.headers) {
|
||||
res.setHeader(name, error.headers[name])
|
||||
}
|
||||
}
|
||||
|
||||
// Send Response
|
||||
res.end(content, 'utf-8')
|
||||
}
|
||||
|
||||
// Check if request accepts JSON
|
||||
const hasReqHeader = (header, includes) =>
|
||||
req.headers[header] && req.headers[header].toLowerCase().includes(includes)
|
||||
const isJson =
|
||||
hasReqHeader('accept', 'application/json') ||
|
||||
hasReqHeader('user-agent', 'curl/')
|
||||
|
||||
// Use basic errors when debug mode is disabled
|
||||
if (!options.debug) {
|
||||
// We hide actual errors from end users, so show them on server logs
|
||||
if (error.statusCode !== 404) {
|
||||
consola.error(error)
|
||||
}
|
||||
|
||||
// Json format is compatible with Youch json responses
|
||||
const json = {
|
||||
status: error.statusCode,
|
||||
message: error.message,
|
||||
name: error.name
|
||||
}
|
||||
if (isJson) {
|
||||
sendResponse(JSON.stringify(json, undefined, 2), 'text/json')
|
||||
return
|
||||
}
|
||||
const html = resources.errorTemplate(json)
|
||||
sendResponse(html)
|
||||
return
|
||||
}
|
||||
|
||||
// Show stack trace
|
||||
const youch = new Youch(
|
||||
error,
|
||||
req,
|
||||
readSource,
|
||||
options.router.base,
|
||||
true
|
||||
)
|
||||
if (isJson) {
|
||||
const json = await youch.toJSON()
|
||||
sendResponse(JSON.stringify(json, undefined, 2), 'text/json')
|
||||
return
|
||||
}
|
||||
|
||||
const html = await youch.toHTML()
|
||||
sendResponse(html)
|
||||
}
|
||||
|
||||
const sanitizeName = name => name ? name.replace('webpack:///', '').split('?')[0] : null
|
||||
|
||||
const normalizeError = (_error, { srcDir, rootDir, buildDir }) => {
|
||||
if (typeof _error === 'string') {
|
||||
_error = { message: _error }
|
||||
} else if (!_error) {
|
||||
_error = { message: '<empty>' }
|
||||
}
|
||||
|
||||
const error = new Error()
|
||||
error.message = _error.message
|
||||
error.name = _error.name
|
||||
error.statusCode = _error.statusCode || 500
|
||||
error.headers = _error.headers
|
||||
|
||||
const searchPath = [
|
||||
srcDir,
|
||||
rootDir,
|
||||
path.join(buildDir, 'dist', 'server'),
|
||||
buildDir,
|
||||
process.cwd()
|
||||
]
|
||||
|
||||
const findInPaths = (fileName) => {
|
||||
for (const dir of searchPath) {
|
||||
const fullPath = path.resolve(dir, fileName)
|
||||
if (fs.existsSync(fullPath)) {
|
||||
return fullPath
|
||||
}
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
|
||||
error.stack = (_error.stack || '')
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const match = line.match(/\(([^)]+)\)|([^\s]+\.[^\s]+):/)
|
||||
if (!match) {
|
||||
return line
|
||||
}
|
||||
const src = match[1] || match[2] || ''
|
||||
return line.replace(src, findInPaths(sanitizeName(src)))
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
async function readSource (frame) {
|
||||
if (fs.existsSync(frame.fileName)) {
|
||||
frame.fullPath = frame.fileName // Youch BW compat
|
||||
frame.contents = await fs.readFile(frame.fileName, 'utf-8')
|
||||
}
|
||||
}
|
155
packages/nuxt3/src/server/middleware/nuxt.ts
Normal file
155
packages/nuxt3/src/server/middleware/nuxt.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import generateETag from 'etag'
|
||||
import fresh from 'fresh'
|
||||
import consola from 'consola'
|
||||
|
||||
import { getContext, TARGETS } from 'src/utils'
|
||||
|
||||
export default ({ options, nuxt, renderRoute, resources }) => async function nuxtMiddleware (req, res, next) {
|
||||
// Get context
|
||||
const context = getContext(req, res)
|
||||
|
||||
try {
|
||||
const url = decodeURI(req.url)
|
||||
res.statusCode = 200
|
||||
const result = await renderRoute(url, context)
|
||||
|
||||
// If result is falsy, call renderLoading
|
||||
if (!result) {
|
||||
await nuxt.callHook('server:nuxt:renderLoading', req, res)
|
||||
return
|
||||
}
|
||||
|
||||
await nuxt.callHook('render:route', url, result, context)
|
||||
const {
|
||||
html,
|
||||
cspScriptSrcHashes,
|
||||
error,
|
||||
redirected,
|
||||
preloadFiles
|
||||
} = result
|
||||
|
||||
if (redirected && context.target !== TARGETS.static) {
|
||||
await nuxt.callHook('render:routeDone', url, result, context)
|
||||
return html
|
||||
}
|
||||
if (error) {
|
||||
res.statusCode = context.nuxt.error.statusCode || 500
|
||||
}
|
||||
|
||||
// Add ETag header
|
||||
if (!error && options.render.etag) {
|
||||
const { hash } = options.render.etag
|
||||
const etag = hash ? hash(html, options.render.etag) : generateETag(html, options.render.etag)
|
||||
if (fresh(req.headers, { etag })) {
|
||||
res.statusCode = 304
|
||||
await nuxt.callHook('render:beforeResponse', url, result, context)
|
||||
res.end()
|
||||
await nuxt.callHook('render:routeDone', url, result, context)
|
||||
return
|
||||
}
|
||||
res.setHeader('ETag', etag)
|
||||
}
|
||||
|
||||
// HTTP2 push headers for preload assets
|
||||
if (!error && options.render.http2.push) {
|
||||
// Parse resourceHints to extract HTTP.2 prefetch/push headers
|
||||
// https://w3c.github.io/preload/#server-push-http-2
|
||||
const { shouldPush, pushAssets } = options.render.http2
|
||||
const { publicPath } = resources.clientManifest
|
||||
|
||||
const links = pushAssets
|
||||
? pushAssets(req, res, publicPath, preloadFiles)
|
||||
: defaultPushAssets(preloadFiles, shouldPush, publicPath, options)
|
||||
|
||||
// Pass with single Link header
|
||||
// https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header
|
||||
// https://www.w3.org/Protocols/9707-link-header.html
|
||||
if (links.length > 0) {
|
||||
res.setHeader('Link', links.join(', '))
|
||||
}
|
||||
}
|
||||
|
||||
if (options.render.csp && cspScriptSrcHashes) {
|
||||
const { allowedSources, policies } = options.render.csp
|
||||
const isReportOnly = !!options.render.csp.reportOnly
|
||||
const cspHeader = isReportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
|
||||
|
||||
res.setHeader(cspHeader, getCspString({ cspScriptSrcHashes, allowedSources, policies, isDev: options.dev, isReportOnly }))
|
||||
}
|
||||
|
||||
// Send response
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
res.setHeader('Accept-Ranges', 'none') // #3870
|
||||
res.setHeader('Content-Length', Buffer.byteLength(html))
|
||||
await nuxt.callHook('render:beforeResponse', url, result, context)
|
||||
res.end(html, 'utf8')
|
||||
await nuxt.callHook('render:routeDone', url, result, context)
|
||||
return html
|
||||
} catch (err) {
|
||||
if (context && context.redirected) {
|
||||
consola.error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if (err.name === 'URIError') {
|
||||
err.statusCode = 400
|
||||
}
|
||||
next(err)
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPushAssets = (preloadFiles, shouldPush, publicPath, options) => {
|
||||
if (shouldPush && options.dev) {
|
||||
consola.warn('http2.shouldPush is deprecated. Use http2.pushAssets function')
|
||||
}
|
||||
|
||||
const links = []
|
||||
preloadFiles.forEach(({ file, asType, fileWithoutQuery, modern }) => {
|
||||
// By default, we only preload scripts or css
|
||||
if (!shouldPush && asType !== 'script' && asType !== 'style') {
|
||||
return
|
||||
}
|
||||
|
||||
// User wants to explicitly control what to preload
|
||||
if (shouldPush && !shouldPush(fileWithoutQuery, asType)) {
|
||||
return
|
||||
}
|
||||
|
||||
const { crossorigin } = options.render
|
||||
const cors = `${crossorigin ? ` crossorigin=${crossorigin};` : ''}`
|
||||
// `modulepreload` rel attribute only supports script-like `as` value
|
||||
// https://html.spec.whatwg.org/multipage/links.html#link-type-modulepreload
|
||||
const rel = modern && asType === 'script' ? 'modulepreload' : 'preload'
|
||||
|
||||
links.push(`<${publicPath}${file}>; rel=${rel};${cors} as=${asType}`)
|
||||
})
|
||||
return links
|
||||
}
|
||||
|
||||
const getCspString = ({ cspScriptSrcHashes, allowedSources, policies, isDev, isReportOnly }) => {
|
||||
const joinedHashes = cspScriptSrcHashes.join(' ')
|
||||
const baseCspStr = `script-src 'self'${isDev ? ' \'unsafe-eval\'' : ''} ${joinedHashes}`
|
||||
const policyObjectAvailable = typeof policies === 'object' && policies !== null && !Array.isArray(policies)
|
||||
|
||||
if (Array.isArray(allowedSources) && allowedSources.length) {
|
||||
return isReportOnly && policyObjectAvailable && !!policies['report-uri'] ? `${baseCspStr} ${allowedSources.join(' ')}; report-uri ${policies['report-uri']};` : `${baseCspStr} ${allowedSources.join(' ')}`
|
||||
}
|
||||
|
||||
if (policyObjectAvailable) {
|
||||
const transformedPolicyObject = transformPolicyObject(policies, cspScriptSrcHashes)
|
||||
return Object.entries(transformedPolicyObject).map(([k, v]) => `${k} ${Array.isArray(v) ? v.join(' ') : v}`).join('; ')
|
||||
}
|
||||
|
||||
return baseCspStr
|
||||
}
|
||||
|
||||
const transformPolicyObject = (policies, cspScriptSrcHashes) => {
|
||||
const userHasDefinedScriptSrc = policies['script-src'] && Array.isArray(policies['script-src'])
|
||||
|
||||
const additionalPolicies = userHasDefinedScriptSrc ? policies['script-src'] : []
|
||||
|
||||
// Self is always needed for inline-scripts, so add it, no matter if the user specified script-src himself.
|
||||
const hashAndPolicyList = cspScriptSrcHashes.concat('\'self\'', additionalPolicies)
|
||||
|
||||
return { ...policies, 'script-src': hashAndPolicyList }
|
||||
}
|
56
packages/nuxt3/src/server/middleware/timing.ts
Normal file
56
packages/nuxt3/src/server/middleware/timing.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import consola from 'consola'
|
||||
import onHeaders from 'on-headers'
|
||||
import { Timer } from 'src/utils'
|
||||
|
||||
export default options => (req, res, next) => {
|
||||
if (res.timing) {
|
||||
consola.warn('server-timing is already registered.')
|
||||
}
|
||||
res.timing = new ServerTiming()
|
||||
|
||||
if (options && options.total) {
|
||||
res.timing.start('total', 'Nuxt Server Time')
|
||||
}
|
||||
|
||||
onHeaders(res, () => {
|
||||
res.timing.end('total')
|
||||
|
||||
if (res.timing.headers.length > 0) {
|
||||
res.setHeader(
|
||||
'Server-Timing',
|
||||
[]
|
||||
.concat(res.getHeader('Server-Timing') || [])
|
||||
.concat(res.timing.headers)
|
||||
.join(', ')
|
||||
)
|
||||
}
|
||||
res.timing.clear()
|
||||
})
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
class ServerTiming extends Timer {
|
||||
constructor (...args) {
|
||||
super(...args)
|
||||
this.headers = []
|
||||
}
|
||||
|
||||
end (...args) {
|
||||
const time = super.end(...args)
|
||||
if (time) {
|
||||
this.headers.push(this.formatHeader(time))
|
||||
}
|
||||
return time
|
||||
}
|
||||
|
||||
clear () {
|
||||
super.clear()
|
||||
this.headers.length = 0
|
||||
}
|
||||
|
||||
formatHeader (time) {
|
||||
const desc = time.description ? `;desc="${time.description}"` : ''
|
||||
return `${time.name};dur=${time.duration}${desc}`
|
||||
}
|
||||
}
|
6
packages/nuxt3/src/server/package.ts
Normal file
6
packages/nuxt3/src/server/package.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
build: true,
|
||||
rollup: {
|
||||
externals: ['jsdom']
|
||||
}
|
||||
}
|
388
packages/nuxt3/src/server/server.ts
Normal file
388
packages/nuxt3/src/server/server.ts
Normal file
@ -0,0 +1,388 @@
|
||||
import path from 'path'
|
||||
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 { VueRenderer } from 'src/vue-renderer'
|
||||
|
||||
import ServerContext from './context'
|
||||
import renderAndGetWindow from './jsdom'
|
||||
import nuxtMiddleware from './middleware/nuxt'
|
||||
import errorMiddleware from './middleware/error'
|
||||
import Listener from './listener'
|
||||
import createTimingMiddleware from './middleware/timing'
|
||||
|
||||
export default class Server {
|
||||
constructor (nuxt) {
|
||||
this.nuxt = nuxt
|
||||
this.options = nuxt.options
|
||||
|
||||
this.globals = determineGlobals(nuxt.options.globalName, nuxt.options.globals)
|
||||
|
||||
this.publicPath = isUrl(this.options.build.publicPath)
|
||||
? this.options.build._publicPath
|
||||
: this.options.build.publicPath
|
||||
|
||||
// Runtime shared resources
|
||||
this.resources = {}
|
||||
|
||||
// Will be set after listen
|
||||
this.listeners = []
|
||||
|
||||
// Create new connect instance
|
||||
this.app = connect()
|
||||
|
||||
// Close hook
|
||||
this.nuxt.hook('close', () => this.close())
|
||||
|
||||
// devMiddleware placeholder
|
||||
if (this.options.dev) {
|
||||
this.nuxt.hook('server:devMiddleware', (devMiddleware) => {
|
||||
this.devMiddleware = devMiddleware
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async ready () {
|
||||
if (this._readyCalled) {
|
||||
return this
|
||||
}
|
||||
this._readyCalled = true
|
||||
|
||||
await this.nuxt.callHook('render:before', this, this.options.render)
|
||||
|
||||
// Initialize vue-renderer
|
||||
this.serverContext = new ServerContext(this)
|
||||
this.renderer = new VueRenderer(this.serverContext)
|
||||
await this.renderer.ready()
|
||||
|
||||
// Setup nuxt middleware
|
||||
await this.setupMiddleware()
|
||||
|
||||
// Call done hook
|
||||
await this.nuxt.callHook('render:done', this)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
async setupMiddleware () {
|
||||
// Apply setupMiddleware from modules first
|
||||
await this.nuxt.callHook('render:setupMiddleware', this.app)
|
||||
|
||||
// Compression middleware for production
|
||||
if (!this.options.dev) {
|
||||
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')
|
||||
this.useMiddleware(compression(compressor))
|
||||
} else if (compressor) {
|
||||
// Else, require own compression middleware if compressor is actually truthy
|
||||
this.useMiddleware(compressor)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.server.timing) {
|
||||
this.useMiddleware(createTimingMiddleware(this.options.server.timing))
|
||||
}
|
||||
|
||||
// For serving static/ files to /
|
||||
const staticMiddleware = serveStatic(
|
||||
path.resolve(this.options.srcDir, this.options.dir.static),
|
||||
this.options.render.static
|
||||
)
|
||||
staticMiddleware.prefix = this.options.render.static.prefix
|
||||
this.useMiddleware(staticMiddleware)
|
||||
|
||||
// Serve .nuxt/dist/client files only for production
|
||||
// For dev they will be served with devMiddleware
|
||||
if (!this.options.dev) {
|
||||
const distDir = path.resolve(this.options.buildDir, 'dist', 'client')
|
||||
this.useMiddleware({
|
||||
path: this.publicPath,
|
||||
handler: serveStatic(
|
||||
distDir,
|
||||
this.options.render.dist
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Dev middleware
|
||||
if (this.options.dev) {
|
||||
this.useMiddleware((req, res, next) => {
|
||||
if (!this.devMiddleware) {
|
||||
return next()
|
||||
}
|
||||
this.devMiddleware(req, res, next)
|
||||
})
|
||||
|
||||
// open in editor for debug mode only
|
||||
if (this.options.debug) {
|
||||
this.useMiddleware({
|
||||
path: '__open-in-editor',
|
||||
handler: launchMiddleware(this.options.editor)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add user provided middleware
|
||||
for (const m of this.options.serverMiddleware) {
|
||||
this.useMiddleware(m)
|
||||
}
|
||||
|
||||
// Graceful 404 error handler
|
||||
const { fallback } = this.options.render
|
||||
if (fallback) {
|
||||
// Dist files
|
||||
if (fallback.dist) {
|
||||
this.useMiddleware({
|
||||
path: this.publicPath,
|
||||
handler: servePlaceholder(fallback.dist)
|
||||
})
|
||||
}
|
||||
|
||||
// Other paths
|
||||
if (fallback.static) {
|
||||
this.useMiddleware({
|
||||
path: '/',
|
||||
handler: servePlaceholder(fallback.static)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Finally use nuxtMiddleware
|
||||
this.useMiddleware(nuxtMiddleware({
|
||||
options: this.options,
|
||||
nuxt: this.nuxt,
|
||||
renderRoute: this.renderRoute.bind(this),
|
||||
resources: this.resources
|
||||
}))
|
||||
|
||||
// Apply errorMiddleware from modules first
|
||||
await this.nuxt.callHook('render:errorMiddleware', this.app)
|
||||
|
||||
// Error middleware for errors that occurred in middleware that declared above
|
||||
this.useMiddleware(errorMiddleware({
|
||||
resources: this.resources,
|
||||
options: this.options
|
||||
}))
|
||||
}
|
||||
|
||||
_normalizeMiddleware (middleware) {
|
||||
// Normalize plain function
|
||||
if (typeof middleware === 'function') {
|
||||
middleware = { handle: middleware }
|
||||
}
|
||||
|
||||
// If a plain string provided as path to middleware
|
||||
if (typeof middleware === 'string') {
|
||||
middleware = this._requireMiddleware(middleware)
|
||||
}
|
||||
|
||||
// Normalize handler to handle (backward compatibility)
|
||||
if (middleware.handler && !middleware.handle) {
|
||||
middleware.handle = middleware.handler
|
||||
delete middleware.handler
|
||||
}
|
||||
|
||||
// Normalize path to route (backward compatibility)
|
||||
if (middleware.path && !middleware.route) {
|
||||
middleware.route = middleware.path
|
||||
delete middleware.path
|
||||
}
|
||||
|
||||
// If handle is a string pointing to path
|
||||
if (typeof middleware.handle === 'string') {
|
||||
Object.assign(middleware, this._requireMiddleware(middleware.handle))
|
||||
}
|
||||
|
||||
// No handle
|
||||
if (!middleware.handle) {
|
||||
middleware.handle = (req, res, next) => {
|
||||
next(new Error('ServerMiddleware should expose a handle: ' + middleware.entry))
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix on handle (proxy-module)
|
||||
if (middleware.handle.prefix !== undefined && middleware.prefix === undefined) {
|
||||
middleware.prefix = middleware.handle.prefix
|
||||
}
|
||||
|
||||
// sub-app (express)
|
||||
if (typeof middleware.handle.handle === 'function') {
|
||||
const server = middleware.handle
|
||||
middleware.handle = server.handle.bind(server)
|
||||
}
|
||||
|
||||
return middleware
|
||||
}
|
||||
|
||||
_requireMiddleware (entry) {
|
||||
// Resolve entry
|
||||
entry = this.nuxt.resolver.resolvePath(entry)
|
||||
|
||||
// Require middleware
|
||||
let middleware
|
||||
try {
|
||||
middleware = this.nuxt.resolver.requireModule(entry)
|
||||
} catch (error) {
|
||||
// Show full error
|
||||
consola.error('ServerMiddleware Error:', error)
|
||||
|
||||
// Placeholder for error
|
||||
middleware = (req, res, next) => { next(error) }
|
||||
}
|
||||
|
||||
// Normalize
|
||||
middleware = this._normalizeMiddleware(middleware)
|
||||
|
||||
// Set entry
|
||||
middleware.entry = entry
|
||||
|
||||
return middleware
|
||||
}
|
||||
|
||||
resolveMiddleware (middleware, fallbackRoute = '/') {
|
||||
// Ensure middleware is normalized
|
||||
middleware = this._normalizeMiddleware(middleware)
|
||||
|
||||
// Fallback route
|
||||
if (!middleware.route) {
|
||||
middleware.route = fallbackRoute
|
||||
}
|
||||
|
||||
// Resolve final route
|
||||
middleware.route = (
|
||||
(middleware.prefix !== false ? this.options.router.base : '') +
|
||||
(typeof middleware.route === 'string' ? middleware.route : '')
|
||||
).replace(/\/\//g, '/')
|
||||
|
||||
// Strip trailing slash
|
||||
if (middleware.route.endsWith('/')) {
|
||||
middleware.route = middleware.route.slice(0, -1)
|
||||
}
|
||||
|
||||
// Assign _middleware to handle to make accessible from app.stack
|
||||
middleware.handle._middleware = middleware
|
||||
|
||||
return middleware
|
||||
}
|
||||
|
||||
useMiddleware (middleware) {
|
||||
const { route, handle } = this.resolveMiddleware(middleware)
|
||||
this.app.use(route, handle)
|
||||
}
|
||||
|
||||
replaceMiddleware (query, middleware) {
|
||||
let serverStackItem
|
||||
|
||||
if (typeof query === 'string') {
|
||||
// Search by entry
|
||||
serverStackItem = this.app.stack.find(({ handle }) => handle._middleware && handle._middleware.entry === query)
|
||||
} else {
|
||||
// Search by reference
|
||||
serverStackItem = this.app.stack.find(({ handle }) => handle === query)
|
||||
}
|
||||
|
||||
// Stop if item not found
|
||||
if (!serverStackItem) {
|
||||
return
|
||||
}
|
||||
|
||||
// unload middleware
|
||||
this.unloadMiddleware(serverStackItem)
|
||||
|
||||
// Resolve middleware
|
||||
const { route, handle } = this.resolveMiddleware(middleware, serverStackItem.route)
|
||||
|
||||
// Update serverStackItem
|
||||
serverStackItem.handle = handle
|
||||
|
||||
// Error State
|
||||
serverStackItem.route = route
|
||||
|
||||
// Return updated item
|
||||
return serverStackItem
|
||||
}
|
||||
|
||||
unloadMiddleware ({ handle }) {
|
||||
if (handle._middleware && typeof handle._middleware.unload === 'function') {
|
||||
handle._middleware.unload()
|
||||
}
|
||||
}
|
||||
|
||||
serverMiddlewarePaths () {
|
||||
return this.app.stack.map(({ handle }) => handle._middleware && handle._middleware.entry).filter(Boolean)
|
||||
}
|
||||
|
||||
renderRoute () {
|
||||
return this.renderer.renderRoute.apply(this.renderer, arguments)
|
||||
}
|
||||
|
||||
loadResources () {
|
||||
return this.renderer.loadResources.apply(this.renderer, arguments)
|
||||
}
|
||||
|
||||
renderAndGetWindow (url, opts = {}, {
|
||||
loadingTimeout = 2000,
|
||||
loadedCallback = this.globals.loadedCallback,
|
||||
globals = this.globals
|
||||
} = {}) {
|
||||
return renderAndGetWindow(url, opts, {
|
||||
loadingTimeout,
|
||||
loadedCallback,
|
||||
globals
|
||||
})
|
||||
}
|
||||
|
||||
async listen (port, host, socket) {
|
||||
// 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,
|
||||
host: host || this.options.server.host,
|
||||
socket: socket || this.options.server.socket,
|
||||
https: this.options.server.https,
|
||||
app: this.app,
|
||||
dev: this.options.dev,
|
||||
baseURL: this.options.router.base
|
||||
})
|
||||
|
||||
// Listen
|
||||
await listener.listen()
|
||||
|
||||
// Push listener to this.listeners
|
||||
this.listeners.push(listener)
|
||||
|
||||
await this.nuxt.callHook('listen', listener.server, listener)
|
||||
|
||||
return listener
|
||||
}
|
||||
|
||||
async close () {
|
||||
if (this.__closed) {
|
||||
return
|
||||
}
|
||||
this.__closed = true
|
||||
|
||||
await Promise.all(this.listeners.map(l => l.close()))
|
||||
|
||||
this.listeners = []
|
||||
|
||||
if (typeof this.renderer.close === 'function') {
|
||||
await this.renderer.close()
|
||||
}
|
||||
|
||||
this.app.stack.forEach(this.unloadMiddleware)
|
||||
this.app.removeAllListeners()
|
||||
this.app = null
|
||||
|
||||
for (const key in this.resources) {
|
||||
delete this.resources[key]
|
||||
}
|
||||
}
|
||||
}
|
67
packages/nuxt3/src/utils/cjs.ts
Normal file
67
packages/nuxt3/src/utils/cjs.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { join } from 'path'
|
||||
|
||||
export function isExternalDependency (id) {
|
||||
return /[/\\]node_modules[/\\]/.test(id)
|
||||
}
|
||||
|
||||
export function clearRequireCache (id) {
|
||||
if (isExternalDependency(id)) {
|
||||
return
|
||||
}
|
||||
|
||||
const entry = getRequireCacheItem(id)
|
||||
|
||||
if (!entry) {
|
||||
delete require.cache[id]
|
||||
return
|
||||
}
|
||||
|
||||
if (entry.parent) {
|
||||
entry.parent.children = entry.parent.children.filter(e => e.id !== id)
|
||||
}
|
||||
|
||||
for (const child of entry.children) {
|
||||
clearRequireCache(child.id)
|
||||
}
|
||||
|
||||
delete require.cache[id]
|
||||
}
|
||||
|
||||
export function scanRequireTree (id, files = new Set()) {
|
||||
if (isExternalDependency(id) || files.has(id)) {
|
||||
return files
|
||||
}
|
||||
|
||||
const entry = getRequireCacheItem(id)
|
||||
|
||||
if (!entry) {
|
||||
files.add(id)
|
||||
return files
|
||||
}
|
||||
|
||||
files.add(entry.id)
|
||||
|
||||
for (const child of entry.children) {
|
||||
scanRequireTree(child.id, files)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
export function getRequireCacheItem (id) {
|
||||
try {
|
||||
return require.cache[id]
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
export function tryRequire (id) {
|
||||
try {
|
||||
return require(id)
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getPKG (id) {
|
||||
return tryRequire(join(id, 'package.json'))
|
||||
}
|
9
packages/nuxt3/src/utils/constants.ts
Normal file
9
packages/nuxt3/src/utils/constants.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const TARGETS = {
|
||||
server: 'server',
|
||||
static: 'static'
|
||||
}
|
||||
|
||||
export const MODES = {
|
||||
universal: 'universal',
|
||||
spa: 'spa'
|
||||
}
|
21
packages/nuxt3/src/utils/context.ts
Normal file
21
packages/nuxt3/src/utils/context.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { TARGETS } from './constants'
|
||||
|
||||
export const getContext = function getContext (req, res) {
|
||||
return { req, res }
|
||||
}
|
||||
|
||||
export const determineGlobals = function determineGlobals (globalName, globals) {
|
||||
const _globals = {}
|
||||
for (const global in globals) {
|
||||
if (typeof globals[global] === 'function') {
|
||||
_globals[global] = globals[global](globalName)
|
||||
} else {
|
||||
_globals[global] = globals[global]
|
||||
}
|
||||
}
|
||||
return _globals
|
||||
}
|
||||
|
||||
export const isFullStatic = function (options) {
|
||||
return !options.dev && !options._legacyGenerate && options.target === TARGETS.static && options.render.ssr
|
||||
}
|
11
packages/nuxt3/src/utils/index.ts
Normal file
11
packages/nuxt3/src/utils/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export * from './context'
|
||||
export * from './lang'
|
||||
export * from './locking'
|
||||
export * from './resolve'
|
||||
export * from './route'
|
||||
export * from './serialize'
|
||||
export * from './task'
|
||||
export * from './timer'
|
||||
export * from './cjs'
|
||||
export * from './modern'
|
||||
export * from './constants'
|
44
packages/nuxt3/src/utils/lang.ts
Normal file
44
packages/nuxt3/src/utils/lang.ts
Normal file
@ -0,0 +1,44 @@
|
||||
export const encodeHtml = function encodeHtml (str) {
|
||||
return str.replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
||||
export const isString = obj => typeof obj === 'string' || obj instanceof String
|
||||
|
||||
export const isNonEmptyString = obj => Boolean(obj && isString(obj))
|
||||
|
||||
export const isPureObject = obj => !Array.isArray(obj) && typeof obj === 'object'
|
||||
|
||||
export const isUrl = function isUrl (url) {
|
||||
return ['http', '//'].some(str => url.startsWith(str))
|
||||
}
|
||||
|
||||
export const urlJoin = function urlJoin () {
|
||||
return [].slice
|
||||
.call(arguments)
|
||||
.join('/')
|
||||
.replace(/\/+/g, '/')
|
||||
.replace(':/', '://')
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]
|
||||
|
||||
const WHITESPACE_REPLACEMENTS = [
|
||||
[/[ \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
|
||||
[/\n{3,}/g, '\n\n'], // strip multiple blank lines (1 allowed)
|
||||
[/\n{2,}$/g, '\n'] // strip blank lines EOF (0 allowed)
|
||||
]
|
||||
|
||||
export const stripWhitespace = function stripWhitespace (string) {
|
||||
WHITESPACE_REPLACEMENTS.forEach(([regex, newSubstr]) => {
|
||||
string = string.replace(regex, newSubstr)
|
||||
})
|
||||
return string
|
||||
}
|
102
packages/nuxt3/src/utils/locking.ts
Normal file
102
packages/nuxt3/src/utils/locking.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import path from 'path'
|
||||
import consola from 'consola'
|
||||
import hash from 'hash-sum'
|
||||
import fs from 'fs-extra'
|
||||
import properlock from 'proper-lockfile'
|
||||
import onExit from 'signal-exit'
|
||||
|
||||
export const lockPaths = new Set()
|
||||
|
||||
export const defaultLockOptions = {
|
||||
stale: 30000,
|
||||
onCompromised: err => consola.warn(err)
|
||||
}
|
||||
|
||||
export function getLockOptions (options) {
|
||||
return Object.assign({}, defaultLockOptions, options)
|
||||
}
|
||||
|
||||
export function createLockPath ({ id = 'nuxt', dir, root }) {
|
||||
const sum = hash(`${root}-${dir}`)
|
||||
|
||||
return path.resolve(root, 'node_modules/.cache/nuxt', `${id}-lock-${sum}`)
|
||||
}
|
||||
|
||||
export async function getLockPath (config) {
|
||||
const lockPath = createLockPath(config)
|
||||
|
||||
// the lock is created for the lockPath as ${lockPath}.lock
|
||||
// so the (temporary) lockPath needs to exist
|
||||
await fs.ensureDir(lockPath)
|
||||
|
||||
return lockPath
|
||||
}
|
||||
|
||||
export async function lock ({ id, dir, root, options }) {
|
||||
const lockPath = await getLockPath({ id, dir, root })
|
||||
|
||||
try {
|
||||
const locked = await properlock.check(lockPath)
|
||||
if (locked) {
|
||||
consola.fatal(`A lock with id '${id}' already exists on ${dir}`)
|
||||
}
|
||||
} catch (e) {
|
||||
consola.debug(`Check for an existing lock with id '${id}' on ${dir} failed`, e)
|
||||
}
|
||||
|
||||
let lockWasCompromised = false
|
||||
let release
|
||||
|
||||
try {
|
||||
options = getLockOptions(options)
|
||||
|
||||
const onCompromised = options.onCompromised
|
||||
options.onCompromised = (err) => {
|
||||
onCompromised(err)
|
||||
lockWasCompromised = true
|
||||
}
|
||||
|
||||
release = await properlock.lock(lockPath, options)
|
||||
} catch (e) {}
|
||||
|
||||
if (!release) {
|
||||
consola.warn(`Unable to get a lock with id '${id}' on ${dir} (but will continue)`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!lockPaths.size) {
|
||||
// make sure to always cleanup our temporary lockPaths
|
||||
onExit(() => {
|
||||
for (const lockPath of lockPaths) {
|
||||
fs.removeSync(lockPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
lockPaths.add(lockPath)
|
||||
|
||||
return async function lockRelease () {
|
||||
try {
|
||||
await fs.remove(lockPath)
|
||||
lockPaths.delete(lockPath)
|
||||
|
||||
// release as last so the lockPath is still removed
|
||||
// when it fails on a compromised lock
|
||||
await release()
|
||||
} catch (e) {
|
||||
if (!lockWasCompromised || !e.message.includes('already released')) {
|
||||
consola.debug(e)
|
||||
return
|
||||
}
|
||||
|
||||
// proper-lockfile doesnt remove lockDir when lock is compromised
|
||||
// removing it here could cause the 'other' process to throw an error
|
||||
// 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)) {
|
||||
await fs.remove(lockDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
64
packages/nuxt3/src/utils/modern.ts
Normal file
64
packages/nuxt3/src/utils/modern.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import UAParser from 'ua-parser-js'
|
||||
|
||||
export const ModernBrowsers = {
|
||||
Edge: '16',
|
||||
Firefox: '60',
|
||||
Chrome: '61',
|
||||
'Chrome Headless': '61',
|
||||
Chromium: '61',
|
||||
Iron: '61',
|
||||
Safari: '10.1',
|
||||
Opera: '48',
|
||||
Yandex: '18',
|
||||
Vivaldi: '1.14',
|
||||
'Mobile Safari': '10.3'
|
||||
}
|
||||
|
||||
let semver
|
||||
let __modernBrowsers
|
||||
|
||||
const getModernBrowsers = () => {
|
||||
if (__modernBrowsers) {
|
||||
return __modernBrowsers
|
||||
}
|
||||
|
||||
__modernBrowsers = Object.keys(ModernBrowsers)
|
||||
.reduce((allBrowsers, browser) => {
|
||||
allBrowsers[browser] = semver.coerce(ModernBrowsers[browser])
|
||||
return allBrowsers
|
||||
}, {})
|
||||
return __modernBrowsers
|
||||
}
|
||||
|
||||
export const isModernBrowser = (ua) => {
|
||||
if (!ua) {
|
||||
return false
|
||||
}
|
||||
if (!semver) {
|
||||
semver = require('semver')
|
||||
}
|
||||
const { browser } = UAParser(ua)
|
||||
const browserVersion = semver.coerce(browser.version)
|
||||
if (!browserVersion) {
|
||||
return false
|
||||
}
|
||||
const modernBrowsers = getModernBrowsers()
|
||||
return Boolean(modernBrowsers[browser.name] && semver.gte(browserVersion, modernBrowsers[browser.name]))
|
||||
}
|
||||
|
||||
export const isModernRequest = (req, modernMode = false) => {
|
||||
if (modernMode === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { socket = {}, headers } = req
|
||||
if (socket._modern === undefined) {
|
||||
const ua = headers && headers['user-agent']
|
||||
socket._modern = isModernBrowser(ua)
|
||||
}
|
||||
|
||||
return socket._modern
|
||||
}
|
||||
|
||||
// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
|
||||
export const safariNoModuleFix = '!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();'
|
109
packages/nuxt3/src/utils/resolve.ts
Normal file
109
packages/nuxt3/src/utils/resolve.ts
Normal file
@ -0,0 +1,109 @@
|
||||
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 startsWithSrcAlias = startsWithAlias(['@', '~'])
|
||||
|
||||
export const startsWithRootAlias = startsWithAlias(['@@', '~~'])
|
||||
|
||||
export const isWindows = process.platform.startsWith('win')
|
||||
|
||||
export const wp = function wp (p = '') {
|
||||
if (isWindows) {
|
||||
return p.replace(/\\/g, '\\\\')
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// Kept for backward compat (modules may use it from template context)
|
||||
export const wChunk = function wChunk (p = '') {
|
||||
return p
|
||||
}
|
||||
|
||||
const reqSep = /\//g
|
||||
const sysSep = escapeRegExp(path.sep)
|
||||
const normalize = string => string.replace(reqSep, sysSep)
|
||||
|
||||
export const r = function r (...args) {
|
||||
const lastArg = args[args.length - 1]
|
||||
|
||||
if (startsWithSrcAlias(lastArg)) {
|
||||
return wp(lastArg)
|
||||
}
|
||||
|
||||
return wp(path.resolve(...args.map(normalize)))
|
||||
}
|
||||
|
||||
export const relativeTo = function relativeTo (...args) {
|
||||
const dir = args.shift()
|
||||
|
||||
// Keep webpack inline loader intact
|
||||
if (args[0].includes('!')) {
|
||||
const loaders = args.shift().split('!')
|
||||
|
||||
return loaders.concat(relativeTo(dir, loaders.pop(), ...args)).join('!')
|
||||
}
|
||||
|
||||
// Resolve path
|
||||
const resolvedPath = r(...args)
|
||||
|
||||
// Check if path is an alias
|
||||
if (startsWithSrcAlias(resolvedPath)) {
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
// Make correct relative path
|
||||
let rp = path.relative(dir, resolvedPath)
|
||||
if (rp[0] !== '.') {
|
||||
rp = '.' + path.sep + rp
|
||||
}
|
||||
|
||||
return wp(rp)
|
||||
}
|
||||
|
||||
export function defineAlias (src, target, prop, opts = {}) {
|
||||
const { bind = true, warn = false } = opts
|
||||
|
||||
if (Array.isArray(prop)) {
|
||||
for (const p of prop) {
|
||||
defineAlias(src, target, p, opts)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let targetVal = target[prop]
|
||||
if (bind && typeof targetVal === 'function') {
|
||||
targetVal = targetVal.bind(target)
|
||||
}
|
||||
|
||||
let warned = false
|
||||
|
||||
Object.defineProperty(src, prop, {
|
||||
get: () => {
|
||||
if (warn && !warned) {
|
||||
warned = true
|
||||
consola.warn({
|
||||
message: `'${prop}' is deprecated'`,
|
||||
additional: new Error().stack.split('\n').splice(2).join('\n')
|
||||
})
|
||||
}
|
||||
return targetVal
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isIndex = s => /(.*)\/index\.[^/]+$/.test(s)
|
||||
|
||||
export function isIndexFileAndFolder (pluginFiles) {
|
||||
// Return early in case the matching file count exceeds 2 (index.js + folder)
|
||||
if (pluginFiles.length !== 2) {
|
||||
return false
|
||||
}
|
||||
return pluginFiles.some(isIndex)
|
||||
}
|
||||
|
||||
export const getMainModule = () => {
|
||||
return require.main || (module && module.main) || module
|
||||
}
|
252
packages/nuxt3/src/utils/route.ts
Normal file
252
packages/nuxt3/src/utils/route.ts
Normal file
@ -0,0 +1,252 @@
|
||||
import path from 'path'
|
||||
import get from 'lodash/get'
|
||||
import consola from 'consola'
|
||||
|
||||
import { r } from './resolve'
|
||||
|
||||
export const flatRoutes = function flatRoutes (router, fileName = '', routes = []) {
|
||||
router.forEach((r) => {
|
||||
if ([':', '*'].some(c => r.path.includes(c))) {
|
||||
return
|
||||
}
|
||||
if (r.children) {
|
||||
if (fileName === '' && r.path === '/') {
|
||||
routes.push('/')
|
||||
}
|
||||
|
||||
return flatRoutes(r.children, fileName + r.path + '/', routes)
|
||||
}
|
||||
fileName = fileName.replace(/\/+/g, '/')
|
||||
|
||||
// if child path is already absolute, do not make any concatenations
|
||||
if (r.path && r.path.startsWith('/')) {
|
||||
routes.push(r.path)
|
||||
} else if (r.path === '' && fileName[fileName.length - 1] === '/') {
|
||||
routes.push(fileName.slice(0, -1) + r.path)
|
||||
} else {
|
||||
routes.push(fileName + r.path)
|
||||
}
|
||||
})
|
||||
return routes
|
||||
}
|
||||
|
||||
function cleanChildrenRoutes (routes, isChild = false, routeNameSplitter = '-') {
|
||||
let start = -1
|
||||
const regExpIndex = new RegExp(`${routeNameSplitter}index$`)
|
||||
const routesIndex = []
|
||||
routes.forEach((route) => {
|
||||
if (regExpIndex.test(route.name) || route.name === 'index') {
|
||||
// Save indexOf 'index' key in name
|
||||
const res = route.name.split(routeNameSplitter)
|
||||
const s = res.indexOf('index')
|
||||
start = start === -1 || s < start ? s : start
|
||||
routesIndex.push(res)
|
||||
}
|
||||
})
|
||||
routes.forEach((route) => {
|
||||
route.path = isChild ? route.path.replace('/', '') : route.path
|
||||
if (route.path.includes('?')) {
|
||||
const names = route.name.split(routeNameSplitter)
|
||||
const paths = route.path.split('/')
|
||||
if (!isChild) {
|
||||
paths.shift()
|
||||
} // clean first / for parents
|
||||
routesIndex.forEach((r) => {
|
||||
const i = r.indexOf('index') - start // children names
|
||||
if (i < paths.length) {
|
||||
for (let a = 0; a <= i; a++) {
|
||||
if (a === i) {
|
||||
paths[a] = paths[a].replace('?', '')
|
||||
}
|
||||
if (a < i && names[a] !== r[a]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
route.path = (isChild ? '' : '/') + paths.join('/')
|
||||
}
|
||||
route.name = route.name.replace(regExpIndex, '')
|
||||
if (route.children) {
|
||||
if (route.children.find(child => child.path === '')) {
|
||||
delete route.name
|
||||
}
|
||||
route.children = cleanChildrenRoutes(route.children, true, routeNameSplitter)
|
||||
}
|
||||
})
|
||||
return routes
|
||||
}
|
||||
|
||||
const DYNAMIC_ROUTE_REGEX = /^\/([:*])/
|
||||
|
||||
export const sortRoutes = function sortRoutes (routes) {
|
||||
routes.sort((a, b) => {
|
||||
if (!a.path.length) {
|
||||
return -1
|
||||
}
|
||||
if (!b.path.length) {
|
||||
return 1
|
||||
}
|
||||
// Order: /static, /index, /:dynamic
|
||||
// Match exact route before index: /login before /index/_slug
|
||||
if (a.path === '/') {
|
||||
return DYNAMIC_ROUTE_REGEX.test(b.path) ? -1 : 1
|
||||
}
|
||||
if (b.path === '/') {
|
||||
return DYNAMIC_ROUTE_REGEX.test(a.path) ? 1 : -1
|
||||
}
|
||||
|
||||
let i
|
||||
let res = 0
|
||||
let y = 0
|
||||
let z = 0
|
||||
const _a = a.path.split('/')
|
||||
const _b = b.path.split('/')
|
||||
for (i = 0; i < _a.length; i++) {
|
||||
if (res !== 0) {
|
||||
break
|
||||
}
|
||||
y = _a[i] === '*' ? 2 : _a[i].includes(':') ? 1 : 0
|
||||
z = _b[i] === '*' ? 2 : _b[i].includes(':') ? 1 : 0
|
||||
res = y - z
|
||||
// If a.length >= b.length
|
||||
if (i === _b.length - 1 && res === 0) {
|
||||
// unless * found sort by level, then alphabetically
|
||||
res = _a[i] === '*' ? -1 : (
|
||||
_a.length === _b.length ? a.path.localeCompare(b.path) : (_a.length - _b.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (res === 0) {
|
||||
// unless * found sort by level, then alphabetically
|
||||
res = _a[i - 1] === '*' && _b[i] ? 1 : (
|
||||
_a.length === _b.length ? a.path.localeCompare(b.path) : (_a.length - _b.length)
|
||||
)
|
||||
}
|
||||
return res
|
||||
})
|
||||
|
||||
routes.forEach((route) => {
|
||||
if (route.children) {
|
||||
sortRoutes(route.children)
|
||||
}
|
||||
})
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
export const createRoutes = function createRoutes ({
|
||||
files,
|
||||
srcDir,
|
||||
pagesDir = '',
|
||||
routeNameSplitter = '-',
|
||||
supportedExtensions = ['vue', 'js'],
|
||||
trailingSlash
|
||||
}) {
|
||||
const routes = []
|
||||
files.forEach((file) => {
|
||||
const keys = file
|
||||
.replace(new RegExp(`^${pagesDir}`), '')
|
||||
.replace(new RegExp(`\\.(${supportedExtensions.join('|')})$`), '')
|
||||
.replace(/\/{2,}/g, '/')
|
||||
.split('/')
|
||||
.slice(1)
|
||||
const route = { 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 + routeNameSplitter + sanitizedKey
|
||||
: sanitizedKey
|
||||
route.name += key === '_' ? 'all' : ''
|
||||
route.chunkName = file.replace(new RegExp(`\\.(${supportedExtensions.join('|')})$`), '')
|
||||
const child = parent.find(parentRoute => parentRoute.name === route.name)
|
||||
|
||||
if (child) {
|
||||
child.children = child.children || []
|
||||
parent = child.children
|
||||
route.path = ''
|
||||
} else if (key === 'index' && i + 1 === keys.length) {
|
||||
route.path += i > 0 ? '' : '/'
|
||||
} else {
|
||||
route.path += '/' + getRoutePathExtension(key)
|
||||
|
||||
if (key.startsWith('_') && key.length > 1) {
|
||||
route.path += '?'
|
||||
}
|
||||
}
|
||||
})
|
||||
if (trailingSlash !== undefined) {
|
||||
route.pathToRegexpOptions = { ...route.pathToRegexpOptions, strict: true }
|
||||
route.path = route.path.replace(/\/+$/, '') + (trailingSlash ? '/' : '') || '/'
|
||||
}
|
||||
|
||||
parent.push(route)
|
||||
})
|
||||
|
||||
sortRoutes(routes)
|
||||
return cleanChildrenRoutes(routes, false, routeNameSplitter)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
if (
|
||||
dir1 &&
|
||||
dir2 &&
|
||||
(
|
||||
dir1 === dir2 ||
|
||||
(
|
||||
dir1.startsWith(dir2) &&
|
||||
!path.basename(dir1).startsWith(path.basename(dir2))
|
||||
)
|
||||
)
|
||||
) {
|
||||
const errorMessage = `options.${key2} cannot be a parent of or same as ${key1}`
|
||||
consola.fatal(errorMessage)
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const getRoutePathExtension = (key) => {
|
||||
if (key === '_') {
|
||||
return '*'
|
||||
}
|
||||
|
||||
if (key.startsWith('_')) {
|
||||
return `:${key.substr(1)}`
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
export const promisifyRoute = function promisifyRoute (fn, ...args) {
|
||||
// If routes is an array
|
||||
if (Array.isArray(fn)) {
|
||||
return Promise.resolve(fn)
|
||||
}
|
||||
// If routes is a function expecting a callback
|
||||
if (fn.length === arguments.length) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fn((err, routeParams) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
resolve(routeParams)
|
||||
}, ...args)
|
||||
})
|
||||
}
|
||||
let promise = fn(...args)
|
||||
if (
|
||||
!promise ||
|
||||
(!(promise instanceof Promise) && typeof promise.then !== 'function')
|
||||
) {
|
||||
promise = Promise.resolve(promise)
|
||||
}
|
||||
return promise
|
||||
}
|
52
packages/nuxt3/src/utils/serialize.ts
Normal file
52
packages/nuxt3/src/utils/serialize.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import serialize from 'serialize-javascript'
|
||||
|
||||
export function normalizeFunctions (obj) {
|
||||
if (typeof obj !== 'object' || Array.isArray(obj) || obj === null) {
|
||||
return obj
|
||||
}
|
||||
for (const key in obj) {
|
||||
if (key === '__proto__' || key === 'constructor') {
|
||||
continue
|
||||
}
|
||||
const val = obj[key]
|
||||
if (val !== null && typeof val === 'object' && !Array.isArray(obj)) {
|
||||
obj[key] = normalizeFunctions(val)
|
||||
}
|
||||
if (typeof obj[key] === 'function') {
|
||||
const asString = obj[key].toString()
|
||||
const match = asString.match(/^([^{(]+)=>\s*([\0-\uFFFF]*)/)
|
||||
if (match) {
|
||||
const fullFunctionBody = match[2].match(/^{?(\s*return\s+)?([\0-\uFFFF]*?)}?$/)
|
||||
let functionBody = fullFunctionBody[2].trim()
|
||||
if (fullFunctionBody[1] || !match[2].trim().match(/^\s*{/)) {
|
||||
functionBody = `return ${functionBody}`
|
||||
}
|
||||
// eslint-disable-next-line no-new-func
|
||||
obj[key] = new Function(...match[1].split(',').map(arg => arg.trim()), functionBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
export function serializeFunction (func) {
|
||||
let open = false
|
||||
func = normalizeFunctions(func)
|
||||
return serialize(func)
|
||||
.replace(serializeFunction.assignmentRE, (_, spaces) => {
|
||||
return `${spaces}: function (`
|
||||
})
|
||||
.replace(serializeFunction.internalFunctionRE, (_, spaces, name, args) => {
|
||||
if (open) {
|
||||
return `${spaces}${name}: function (${args}) {`
|
||||
} else {
|
||||
open = true
|
||||
return _
|
||||
}
|
||||
})
|
||||
.replace(`${func.name || 'function'}(`, 'function (')
|
||||
.replace('function function', 'function')
|
||||
}
|
||||
|
||||
serializeFunction.internalFunctionRE = /^(\s*)(?!(?:if)|(?:for)|(?:while)|(?:switch)|(?:catch))(\w+)\s*\((.*?)\)\s*\{/gm
|
||||
serializeFunction.assignmentRE = /^(\s*):(\w+)\(/gm
|
36
packages/nuxt3/src/utils/task.ts
Normal file
36
packages/nuxt3/src/utils/task.ts
Normal file
@ -0,0 +1,36 @@
|
||||
export const sequence = function sequence (tasks, fn) {
|
||||
return tasks.reduce(
|
||||
(promise, task) => promise.then(() => fn(task)),
|
||||
Promise.resolve()
|
||||
)
|
||||
}
|
||||
|
||||
export const parallel = function parallel (tasks, fn) {
|
||||
return Promise.all(tasks.map(fn))
|
||||
}
|
||||
|
||||
export const chainFn = function chainFn (base, fn) {
|
||||
if (typeof fn !== 'function') {
|
||||
return base
|
||||
}
|
||||
return function (...args) {
|
||||
if (typeof base !== 'function') {
|
||||
return fn.apply(this, args)
|
||||
}
|
||||
let baseResult = base.apply(this, args)
|
||||
// Allow function to mutate the first argument instead of returning the result
|
||||
if (baseResult === undefined) {
|
||||
[baseResult] = args
|
||||
}
|
||||
const fnResult = fn.call(
|
||||
this,
|
||||
baseResult,
|
||||
...Array.prototype.slice.call(args, 1)
|
||||
)
|
||||
// Return mutated argument if no result was returned
|
||||
if (fnResult === undefined) {
|
||||
return baseResult
|
||||
}
|
||||
return fnResult
|
||||
}
|
||||
}
|
65
packages/nuxt3/src/utils/timer.ts
Normal file
65
packages/nuxt3/src/utils/timer.ts
Normal file
@ -0,0 +1,65 @@
|
||||
async function promiseFinally (fn, finalFn) {
|
||||
let result
|
||||
try {
|
||||
if (typeof fn === 'function') {
|
||||
result = await fn()
|
||||
} else {
|
||||
result = await fn
|
||||
}
|
||||
} finally {
|
||||
finalFn()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const timeout = function timeout (fn, ms, msg) {
|
||||
let timerId
|
||||
const warpPromise = promiseFinally(fn, () => clearTimeout(timerId))
|
||||
const timerPromise = new Promise((resolve, reject) => {
|
||||
timerId = setTimeout(() => reject(new Error(msg)), ms)
|
||||
})
|
||||
return Promise.race([warpPromise, timerPromise])
|
||||
}
|
||||
|
||||
export const waitFor = function waitFor (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms || 0))
|
||||
}
|
||||
export class Timer {
|
||||
constructor () {
|
||||
this._times = new Map()
|
||||
}
|
||||
|
||||
start (name, description) {
|
||||
const time = {
|
||||
name,
|
||||
description,
|
||||
start: this.hrtime()
|
||||
}
|
||||
this._times.set(name, time)
|
||||
return time
|
||||
}
|
||||
|
||||
end (name) {
|
||||
if (this._times.has(name)) {
|
||||
const time = this._times.get(name)
|
||||
time.duration = this.hrtime(time.start)
|
||||
this._times.delete(name)
|
||||
return time
|
||||
}
|
||||
}
|
||||
|
||||
hrtime (start) {
|
||||
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)
|
||||
}
|
||||
return useBigInt ? process.hrtime.bigint() : process.hrtime()
|
||||
}
|
||||
|
||||
clear () {
|
||||
this._times.clear()
|
||||
}
|
||||
}
|
46
packages/nuxt3/src/vue-app/app.pages.vue
Normal file
46
packages/nuxt3/src/vue-app/app.pages.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
Has pages/ ? <nuxt-page />
|
||||
Please create `pages/index.vue` or `app.vue`
|
||||
</template>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<nav>My navbar</nav>
|
||||
<nuxt-page />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/*
|
||||
my-project/app.vue -> no router needed
|
||||
|
||||
vs
|
||||
|
||||
my-project/pages/index.vue -> router needed and app.vue display
|
||||
|
||||
vue-app/dotnuxt/app.vue
|
||||
|
||||
Resolving App:
|
||||
1. ~/app.vue (variable)
|
||||
2. (if pages/) nuxt-app/app.pages.vue (Light with router)
|
||||
3. nuxt-app/app.tutorial.vue
|
||||
|
||||
For layers:
|
||||
create {srcDir}/app.vue:
|
||||
<template>
|
||||
<nuxt-layer>
|
||||
|
||||
app.starter.vue -- guiding to create app.vue or pages/
|
||||
app.default.vue if (pages/)
|
||||
app.layout.vue if (layouts/)
|
||||
|
||||
pages/index.vue:
|
||||
<nuxt-layout>
|
||||
...
|
||||
</nuxt-layout>
|
||||
|
||||
pages/about.vue:
|
||||
...
|
||||
|
||||
*/
|
||||
</script>
|
0
packages/nuxt3/src/vue-app/app.tutorial.vue
Normal file
0
packages/nuxt3/src/vue-app/app.tutorial.vue
Normal file
1
packages/nuxt3/src/vue-app/components/index.js
Normal file
1
packages/nuxt3/src/vue-app/components/index.js
Normal file
@ -0,0 +1 @@
|
||||
// nothing here
|
1
packages/nuxt3/src/vue-app/index.js
Normal file
1
packages/nuxt3/src/vue-app/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { init } from './nuxt'
|
28
packages/nuxt3/src/vue-app/nuxt.js
Normal file
28
packages/nuxt3/src/vue-app/nuxt.js
Normal file
@ -0,0 +1,28 @@
|
||||
import Hookable from 'hookable'
|
||||
import { defineGetter } from './utils'
|
||||
|
||||
class Nuxt extends Hookable {
|
||||
constructor ({ app, ssrContext, globalName }) {
|
||||
super()
|
||||
this.app = app
|
||||
this.ssrContext = ssrContext
|
||||
this.globalName = globalName
|
||||
}
|
||||
|
||||
provide (name, value) {
|
||||
const $name = '$' + name
|
||||
defineGetter(this.app, $name, value)
|
||||
defineGetter(this.app.config.globalProperties, $name, value)
|
||||
}
|
||||
}
|
||||
|
||||
export async function init ({ app, plugins, ssrContext, globalName = 'nuxt' }) {
|
||||
const nuxt = new Nuxt({ app, ssrContext, globalName })
|
||||
nuxt.provide('nuxt', nuxt)
|
||||
|
||||
const inject = nuxt.provide.bind(nuxt)
|
||||
|
||||
for (const plugin of plugins) {
|
||||
await plugin(nuxt, inject)
|
||||
}
|
||||
}
|
25
packages/nuxt3/src/vue-app/nuxt/entry.client.js
Normal file
25
packages/nuxt3/src/vue-app/nuxt/entry.client.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import plugins from './plugins.client'
|
||||
import { init } from 'nuxt-app'
|
||||
import App from '<%= appPath %>'
|
||||
|
||||
async function initApp () {
|
||||
const app = createSSRApp(App)
|
||||
|
||||
await init({
|
||||
app,
|
||||
plugins
|
||||
})
|
||||
|
||||
await app.$nuxt.callHook('client:create')
|
||||
|
||||
app.mount('#__nuxt')
|
||||
|
||||
await app.$nuxt.callHook('client:mounted')
|
||||
|
||||
console.log('App ready:', app) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
initApp().catch((error) => {
|
||||
console.error('Error while mounting app:', error) // eslint-disable-line no-console
|
||||
})
|
19
packages/nuxt3/src/vue-app/nuxt/entry.server.js
Normal file
19
packages/nuxt3/src/vue-app/nuxt/entry.server.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import { init } from 'nuxt-app'
|
||||
import plugins from 'nuxt-build/plugins.server'
|
||||
import App from '<%= appPath %>'
|
||||
|
||||
export default async function createNuxtAppServer (ssrContext = {}) {
|
||||
const app = createApp(App)
|
||||
|
||||
await init({
|
||||
app,
|
||||
plugins,
|
||||
ssrContext
|
||||
})
|
||||
|
||||
await app.$nuxt.callHook('server:create')
|
||||
|
||||
return app
|
||||
}
|
3
packages/nuxt3/src/vue-app/nuxt/layouts/default.vue
Normal file
3
packages/nuxt3/src/vue-app/nuxt/layouts/default.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<Nuxt />
|
||||
</template>
|
5
packages/nuxt3/src/vue-app/nuxt/plugins.client.js
Normal file
5
packages/nuxt3/src/vue-app/nuxt/plugins.client.js
Normal file
@ -0,0 +1,5 @@
|
||||
import sharedPlugins from './plugins'
|
||||
|
||||
export default [
|
||||
...sharedPlugins
|
||||
]
|
11
packages/nuxt3/src/vue-app/nuxt/plugins.js
Normal file
11
packages/nuxt3/src/vue-app/nuxt/plugins.js
Normal file
@ -0,0 +1,11 @@
|
||||
// import router from 'nuxt-app/plugins/router'
|
||||
import state from 'nuxt-app/plugins/state'
|
||||
import components from 'nuxt-app/plugins/components'
|
||||
import legacy from 'nuxt-app/plugins/legacy'
|
||||
|
||||
export default [
|
||||
// router,
|
||||
state,
|
||||
components,
|
||||
legacy
|
||||
]
|
7
packages/nuxt3/src/vue-app/nuxt/plugins.server.js
Normal file
7
packages/nuxt3/src/vue-app/nuxt/plugins.server.js
Normal file
@ -0,0 +1,7 @@
|
||||
import sharedPlugins from './plugins'
|
||||
import preload from 'nuxt-app/plugins/preload'
|
||||
|
||||
export default [
|
||||
...sharedPlugins,
|
||||
preload
|
||||
]
|
19
packages/nuxt3/src/vue-app/nuxt/routes.js
Normal file
19
packages/nuxt3/src/vue-app/nuxt/routes.js
Normal file
@ -0,0 +1,19 @@
|
||||
const Index = () => import('~/pages' /* webpackChunkName: "Home" */)
|
||||
const About = () => import('~/pages/about' /* webpackChunkName: "About" */)
|
||||
const Custom = () => import('~/pages/custom' /* webpackChunkName: "Custom" */)
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
__file: '@/pages/index.vue',
|
||||
component: Index
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
component: About
|
||||
},
|
||||
{
|
||||
path: '/custom',
|
||||
component: Custom
|
||||
}
|
||||
]
|
9
packages/nuxt3/src/vue-app/nuxt/views/app.template.html
Normal file
9
packages/nuxt3/src/vue-app/nuxt/views/app.template.html
Normal file
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html {{ HTML_ATTRS }}>
|
||||
<head {{ HEAD_ATTRS }}>
|
||||
{{ HEAD }}
|
||||
</head>
|
||||
<body {{ BODY_ATTRS }}>
|
||||
<div id="__nuxt">{{ APP }}</div>
|
||||
</body>
|
||||
</html>
|
23
packages/nuxt3/src/vue-app/nuxt/views/error.html
Normal file
23
packages/nuxt3/src/vue-app/nuxt/views/error.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Server error</title>
|
||||
<meta charset="utf-8">
|
||||
<meta content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" name=viewport>
|
||||
<style>
|
||||
.__nuxt-error-page{padding: 1rem;background:#f7f8fb;color:#47494e;text-align:center;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;font-family:sans-serif;font-weight:100!important;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-font-smoothing:antialiased;position:absolute;top:0;left:0;right:0;bottom:0}.__nuxt-error-page .error{max-width:450px}.__nuxt-error-page .title{font-size:24px;font-size:1.5rem;margin-top:15px;color:#47494e;margin-bottom:8px}.__nuxt-error-page .description{color:#7f828b;line-height:21px;margin-bottom:10px}.__nuxt-error-page a{color:#7f828b!important;text-decoration:none}.__nuxt-error-page .logo{position:fixed;left:12px;bottom:12px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="__nuxt-error-page">
|
||||
<div class="error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="90" height="90" fill="#DBE1EC" viewBox="0 0 48 48"><path d="M22 30h4v4h-4zm0-16h4v12h-4zm1.99-10C12.94 4 4 12.95 4 24s8.94 20 19.99 20S44 35.05 44 24 35.04 4 23.99 4zM24 40c-8.84 0-16-7.16-16-16S15.16 8 24 8s16 7.16 16 16-7.16 16-16 16z"/></svg>
|
||||
<div class="title">Server error</div>
|
||||
<div class="description">{{ message }}</div>
|
||||
</div>
|
||||
<div class="logo">
|
||||
<a href="https://nuxtjs.org" target="_blank" rel="noopener">Nuxt.js</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
11
packages/nuxt3/src/vue-app/plugins/components.js
Normal file
11
packages/nuxt3/src/vue-app/plugins/components.js
Normal file
@ -0,0 +1,11 @@
|
||||
// import { h, defineComponent } from 'vue'
|
||||
import { Link } from 'vue-router'
|
||||
|
||||
// const NuxtLink = defineComponent({
|
||||
// extends: Link
|
||||
// })
|
||||
|
||||
export default function components ({ app }) {
|
||||
app.component('NuxtLink', Link)
|
||||
app.component('NLink', Link) // TODO: deprecate
|
||||
}
|
15
packages/nuxt3/src/vue-app/plugins/legacy.js
Normal file
15
packages/nuxt3/src/vue-app/plugins/legacy.js
Normal file
@ -0,0 +1,15 @@
|
||||
export default function legacy ({ app }) {
|
||||
app.$nuxt.context = {}
|
||||
|
||||
if (process.client) {
|
||||
const legacyApp = { ...app }
|
||||
legacyApp.$root = legacyApp
|
||||
window[app.$nuxt.globalName] = legacyApp
|
||||
}
|
||||
|
||||
if (process.server) {
|
||||
const { ssrContext } = app.$nuxt
|
||||
app.$nuxt.context.req = ssrContext.req
|
||||
app.$nuxt.context.res = ssrContext.res
|
||||
}
|
||||
}
|
9
packages/nuxt3/src/vue-app/plugins/preload.js
Normal file
9
packages/nuxt3/src/vue-app/plugins/preload.js
Normal file
@ -0,0 +1,9 @@
|
||||
export default function preload ({ app }) {
|
||||
app.mixin({
|
||||
beforeCreate () {
|
||||
const { _registeredComponents } = this.$nuxt.ssrContext
|
||||
const { __moduleIdentifier } = this.$options
|
||||
_registeredComponents.push(__moduleIdentifier)
|
||||
}
|
||||
})
|
||||
}
|
37
packages/nuxt3/src/vue-app/plugins/router.js
Normal file
37
packages/nuxt3/src/vue-app/plugins/router.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { ref } from 'vue'
|
||||
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
|
||||
|
||||
import routes from 'nuxt-build/routes'
|
||||
|
||||
export default function router ({ app }) {
|
||||
const routerHistory = process.client
|
||||
? createWebHistory()
|
||||
: createMemoryHistory()
|
||||
|
||||
const router = createRouter({
|
||||
history: routerHistory,
|
||||
routes
|
||||
})
|
||||
app.use(router)
|
||||
|
||||
const previousRoute = ref()
|
||||
router.afterEach((to, from) => {
|
||||
previousRoute.value = from
|
||||
})
|
||||
|
||||
Object.defineProperty(app.config.globalProperties, 'previousRoute', {
|
||||
get: () => previousRoute.value
|
||||
})
|
||||
|
||||
if (process.server) {
|
||||
app.$nuxt.hook('server:create', async () => {
|
||||
router.push(app.$nuxt.ssrContext.url)
|
||||
await router.isReady()
|
||||
})
|
||||
} else {
|
||||
app.$nuxt.hook('client:create', async () => {
|
||||
router.push(router.history.location.fullPath)
|
||||
await router.isReady()
|
||||
})
|
||||
}
|
||||
}
|
13
packages/nuxt3/src/vue-app/plugins/state.js
Normal file
13
packages/nuxt3/src/vue-app/plugins/state.js
Normal file
@ -0,0 +1,13 @@
|
||||
export default function state ({ app }) {
|
||||
if (process.server) {
|
||||
app.$nuxt.state = {
|
||||
serverRendered: true
|
||||
// data, fetch, vuex, etc.
|
||||
}
|
||||
app.$nuxt.ssrContext.nuxt = app.$nuxt.state
|
||||
}
|
||||
|
||||
if (process.client) {
|
||||
app.$nuxt.state = window.__NUXT__ || {}
|
||||
}
|
||||
}
|
12
packages/nuxt3/src/vue-app/template.ts
Normal file
12
packages/nuxt3/src/vue-app/template.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import path from 'path'
|
||||
import globby from 'globby'
|
||||
|
||||
const dir = path.join(__dirname, 'nuxt')
|
||||
const files = globby.sync(path.join(dir, '/**'))
|
||||
.map(f => f.replace(dir + path.sep, '')) // TODO: workaround
|
||||
|
||||
export default {
|
||||
dependencies: {},
|
||||
dir,
|
||||
files
|
||||
}
|
3
packages/nuxt3/src/vue-app/utils.js
Normal file
3
packages/nuxt3/src/vue-app/utils.js
Normal file
@ -0,0 +1,3 @@
|
||||
export function defineGetter (obj, key, val) {
|
||||
Object.defineProperty(obj, key, { get: () => val })
|
||||
}
|
38
packages/nuxt3/src/vue-app/vetur/nuxt-attributes.json
Normal file
38
packages/nuxt3/src/vue-app/vetur/nuxt-attributes.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"nuxtChildKey": {
|
||||
"description": "This prop will be set to <router-view/>, useful to make transitions inside a dynamic page and different route. Default: `$route.fullPath`"
|
||||
},
|
||||
"to": {
|
||||
"description": "Denotes the target route of the link. When clicked, the value of the to prop will be passed to router.push() internally, so the value can be either a string or a location descriptor object."
|
||||
},
|
||||
"prefetch": {
|
||||
"type": "boolean",
|
||||
"description": "Prefetch route target (overrides router.prefetchLinks value in nuxt.config.js)."
|
||||
},
|
||||
"no-prefetch": {
|
||||
"description": "Avoid prefetching route target."
|
||||
},
|
||||
"replace": {
|
||||
"type": "boolean",
|
||||
"description": "Setting replace prop will call router.replace() instead of router.push() when clicked, so the navigation will not leave a history record."
|
||||
},
|
||||
"append": {
|
||||
"type": "boolean",
|
||||
"description": "Setting append prop always appends the relative path to the current path. For example, assuming we are navigating from /a to a relative link b, without append we will end up at /b, but with append we will end up at /a/b."
|
||||
},
|
||||
"tag": {
|
||||
"description": "Specify which tag to render to, and it will still listen to click events for navigation."
|
||||
},
|
||||
"active-class": {
|
||||
"description": "Configure the active CSS class applied when the link is active."
|
||||
},
|
||||
"exact": {
|
||||
"description": "The default active class matching behavior is inclusive match. For example, <router-link to=\"/a\"> will get this class applied as long as the current path starts with /a/ or is /a.\nOne consequence of this is that <router-link to=\"/\"> will be active for every route! To force the link into \"exact match mode\", use the exact prop: <router-link to=\"/\" exact>"
|
||||
},
|
||||
"event": {
|
||||
"description": "Specify the event(s) that can trigger the link navigation."
|
||||
},
|
||||
"exact-active-class": {
|
||||
"description": "Configure the active CSS class applied when the link is active with exact match. Note the default value can also be configured globally via the linkExactActiveClass router constructor option."
|
||||
}
|
||||
}
|
47
packages/nuxt3/src/vue-app/vetur/nuxt-tags.json
Normal file
47
packages/nuxt3/src/vue-app/vetur/nuxt-tags.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"nuxt": {
|
||||
"attributes": [
|
||||
"nuxtChildKey"
|
||||
],
|
||||
"description": "Component to render the current nuxt page."
|
||||
},
|
||||
"n-child": {
|
||||
"description": "Component for displaying the children components in a nested route."
|
||||
},
|
||||
"nuxt-child": {
|
||||
"description": "Component for displaying the children components in a nested route."
|
||||
},
|
||||
"n-link": {
|
||||
"attributes": [
|
||||
"to",
|
||||
"replace",
|
||||
"append",
|
||||
"tag",
|
||||
"active-class",
|
||||
"exact",
|
||||
"event",
|
||||
"exact-active-class",
|
||||
"prefetch",
|
||||
"no-prefetch"
|
||||
],
|
||||
"description": "Component for navigating between Nuxt pages."
|
||||
},
|
||||
"nuxt-link": {
|
||||
"attributes": [
|
||||
"to",
|
||||
"replace",
|
||||
"append",
|
||||
"tag",
|
||||
"active-class",
|
||||
"exact",
|
||||
"event",
|
||||
"exact-active-class",
|
||||
"prefetch",
|
||||
"no-prefetch"
|
||||
],
|
||||
"description": "Component for navigating between Nuxt pages."
|
||||
},
|
||||
"no-ssr": {
|
||||
"description": "Component for excluding a part of your app from server-side rendering."
|
||||
}
|
||||
}
|
1
packages/nuxt3/src/vue-renderer/index.ts
Normal file
1
packages/nuxt3/src/vue-renderer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as VueRenderer } from './renderer'
|
386
packages/nuxt3/src/vue-renderer/renderer.ts
Normal file
386
packages/nuxt3/src/vue-renderer/renderer.ts
Normal file
@ -0,0 +1,386 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import consola from 'consola'
|
||||
import template from 'lodash/template'
|
||||
import { TARGETS, isModernRequest, waitFor } from 'src/utils'
|
||||
|
||||
import SPARenderer from './renderers/spa'
|
||||
import SSRRenderer from './renderers/ssr'
|
||||
import ModernRenderer from './renderers/modern'
|
||||
|
||||
export default class VueRenderer {
|
||||
constructor (context) {
|
||||
this.serverContext = context
|
||||
this.options = this.serverContext.options
|
||||
|
||||
// Will be set by createRenderer
|
||||
this.renderer = {
|
||||
ssr: undefined,
|
||||
modern: undefined,
|
||||
spa: undefined
|
||||
}
|
||||
|
||||
// Renderer runtime resources
|
||||
Object.assign(this.serverContext.resources, {
|
||||
clientManifest: undefined,
|
||||
modernManifest: undefined,
|
||||
serverManifest: undefined,
|
||||
ssrTemplate: undefined,
|
||||
spaTemplate: undefined,
|
||||
errorTemplate: this.parseTemplate('Nuxt.js Internal Server Error')
|
||||
})
|
||||
|
||||
// Default status
|
||||
this._state = 'created'
|
||||
this._error = null
|
||||
}
|
||||
|
||||
ready () {
|
||||
if (!this._readyPromise) {
|
||||
this._state = 'loading'
|
||||
this._readyPromise = this._ready()
|
||||
.then(() => {
|
||||
this._state = 'ready'
|
||||
return this
|
||||
})
|
||||
.catch((error) => {
|
||||
this._state = 'error'
|
||||
this._error = error
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
return this._readyPromise
|
||||
}
|
||||
|
||||
async _ready () {
|
||||
// Resolve dist path
|
||||
this.distPath = path.resolve(this.options.buildDir, 'dist', 'server')
|
||||
|
||||
// -- Development mode --
|
||||
if (this.options.dev) {
|
||||
this.serverContext.nuxt.hook('build:resources', mfs => this.loadResources(mfs))
|
||||
return
|
||||
}
|
||||
|
||||
// -- Production mode --
|
||||
|
||||
// Try once to load SSR resources from fs
|
||||
await this.loadResources(fs)
|
||||
|
||||
// Without using `nuxt start` (programmatic, tests and generate)
|
||||
if (!this.options._start) {
|
||||
this.serverContext.nuxt.hook('build:resources', () => this.loadResources(fs))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify resources
|
||||
if (this.options.modern && !this.isModernReady) {
|
||||
throw new Error(
|
||||
`No modern build files found in ${this.distPath}.\nUse either \`nuxt build --modern\` or \`modern\` option to build modern files.`
|
||||
)
|
||||
} else if (!this.isReady) {
|
||||
throw new Error(
|
||||
`No build files found in ${this.distPath}.\nUse either \`nuxt build\` or \`builder.build()\` or start nuxt in development mode.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async loadResources (_fs) {
|
||||
const updated = []
|
||||
|
||||
const readResource = async (fileName, encoding) => {
|
||||
try {
|
||||
const fullPath = path.resolve(this.distPath, fileName)
|
||||
|
||||
if (!await _fs.exists(fullPath)) {
|
||||
return
|
||||
}
|
||||
const contents = await _fs.readFile(fullPath, encoding)
|
||||
|
||||
return contents
|
||||
} catch (err) {
|
||||
consola.error('Unable to load resource:', fileName, err)
|
||||
}
|
||||
}
|
||||
|
||||
for (const resourceName in this.resourceMap) {
|
||||
const { fileName, transform, encoding } = this.resourceMap[resourceName]
|
||||
|
||||
// Load resource
|
||||
let resource = await readResource(fileName, encoding)
|
||||
|
||||
// Skip unavailable resources
|
||||
if (!resource) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply transforms
|
||||
if (typeof transform === 'function') {
|
||||
resource = await transform(resource, { readResource })
|
||||
}
|
||||
|
||||
// Update resource
|
||||
this.serverContext.resources[resourceName] = resource
|
||||
updated.push(resourceName)
|
||||
}
|
||||
|
||||
// Load templates
|
||||
await this.loadTemplates()
|
||||
|
||||
await this.serverContext.nuxt.callHook('render:resourcesLoaded', this.serverContext.resources)
|
||||
|
||||
// Detect if any resource updated
|
||||
if (updated.length > 0) {
|
||||
// Create new renderer
|
||||
this.createRenderer()
|
||||
}
|
||||
}
|
||||
|
||||
async loadTemplates () {
|
||||
// Reload error template
|
||||
const errorTemplatePath = path.resolve(this.options.buildDir, 'views/error.html')
|
||||
|
||||
if (await fs.exists(errorTemplatePath)) {
|
||||
const errorTemplate = await fs.readFile(errorTemplatePath, 'utf8')
|
||||
this.serverContext.resources.errorTemplate = this.parseTemplate(errorTemplate)
|
||||
}
|
||||
|
||||
// Reload loading template
|
||||
const loadingHTMLPath = path.resolve(this.options.buildDir, 'loading.html')
|
||||
|
||||
if (await fs.exists(loadingHTMLPath)) {
|
||||
this.serverContext.resources.loadingHTML = await fs.readFile(loadingHTMLPath, 'utf8')
|
||||
this.serverContext.resources.loadingHTML = this.serverContext.resources.loadingHTML.replace(/\r|\n|[\t\s]{3,}/g, '')
|
||||
} else {
|
||||
this.serverContext.resources.loadingHTML = ''
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove in Nuxt 3
|
||||
get noSSR () { /* Backward compatibility */
|
||||
return this.options.render.ssr === false
|
||||
}
|
||||
|
||||
get SSR () {
|
||||
return this.options.render.ssr === true
|
||||
}
|
||||
|
||||
get isReady () {
|
||||
// SPA
|
||||
if (!this.serverContext.resources.spaTemplate || !this.renderer.spa) {
|
||||
return false
|
||||
}
|
||||
|
||||
// SSR
|
||||
if (this.SSR && (!this.serverContext.resources.ssrTemplate || !this.renderer.ssr)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
get isModernReady () {
|
||||
return this.isReady && this.serverContext.resources.modernManifest
|
||||
}
|
||||
|
||||
// TODO: Remove in Nuxt 3
|
||||
get isResourcesAvailable () { /* Backward compatibility */
|
||||
return this.isReady
|
||||
}
|
||||
|
||||
detectModernBuild () {
|
||||
const { options, resources } = this.serverContext
|
||||
if ([false, 'client', 'server'].includes(options.modern)) {
|
||||
return
|
||||
}
|
||||
|
||||
const isExplicitStaticModern = options.target === TARGETS.static && options.modern
|
||||
if (!resources.modernManifest && !isExplicitStaticModern) {
|
||||
options.modern = false
|
||||
return
|
||||
}
|
||||
|
||||
options.modern = options.render.ssr ? 'server' : 'client'
|
||||
consola.info(`Modern bundles are detected. Modern mode (\`${options.modern}\`) is enabled now.`)
|
||||
}
|
||||
|
||||
createRenderer () {
|
||||
// Resource clientManifest is always required
|
||||
if (!this.serverContext.resources.clientManifest) {
|
||||
return
|
||||
}
|
||||
|
||||
this.detectModernBuild()
|
||||
|
||||
// Create SPA renderer
|
||||
if (this.serverContext.resources.spaTemplate) {
|
||||
this.renderer.spa = new SPARenderer(this.serverContext)
|
||||
}
|
||||
|
||||
// Skip the rest if SSR resources are not available
|
||||
if (this.serverContext.resources.ssrTemplate && this.serverContext.resources.serverManifest) {
|
||||
// Create bundle renderer for SSR
|
||||
this.renderer.ssr = new SSRRenderer(this.serverContext)
|
||||
|
||||
if (this.options.modern !== false) {
|
||||
this.renderer.modern = new ModernRenderer(this.serverContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderSPA (renderContext) {
|
||||
return this.renderer.spa.render(renderContext)
|
||||
}
|
||||
|
||||
renderSSR (renderContext) {
|
||||
// Call renderToString from the bundleRenderer and generate the HTML (will update the renderContext as well)
|
||||
const renderer = renderContext.modern ? this.renderer.modern : this.renderer.ssr
|
||||
return renderer.render(renderContext)
|
||||
}
|
||||
|
||||
async renderRoute (url, renderContext = {}, _retried = 0) {
|
||||
/* istanbul ignore if */
|
||||
if (!this.isReady) {
|
||||
// Fall-back to loading-screen if enabled
|
||||
if (this.options.build.loadingScreen) {
|
||||
// Tell nuxt middleware to use `server:nuxt:renderLoading hook
|
||||
return false
|
||||
}
|
||||
|
||||
// Retry
|
||||
const retryLimit = this.options.dev ? 60 : 3
|
||||
if (_retried < retryLimit && this._state !== 'error') {
|
||||
await this.ready().then(() => waitFor(1000))
|
||||
return this.renderRoute(url, renderContext, _retried + 1)
|
||||
}
|
||||
|
||||
// Throw Error
|
||||
switch (this._state) {
|
||||
case 'created':
|
||||
throw new Error('Renderer ready() is not called! Please ensure `nuxt.ready()` is called and awaited.')
|
||||
case 'loading':
|
||||
throw new Error('Renderer is loading.')
|
||||
case 'error':
|
||||
throw this._error
|
||||
case 'ready':
|
||||
throw new Error(`Renderer resources are not loaded! Please check possible console errors and ensure dist (${this.distPath}) exists.`)
|
||||
default:
|
||||
throw new Error('Renderer is in unknown state!')
|
||||
}
|
||||
}
|
||||
|
||||
// Log rendered url
|
||||
consola.debug(`Rendering url ${url}`)
|
||||
|
||||
// Add url to the renderContext
|
||||
renderContext.url = url
|
||||
// Add target to the renderContext
|
||||
renderContext.target = this.serverContext.nuxt.options.target
|
||||
|
||||
const { req = {}, res = {} } = renderContext
|
||||
|
||||
// renderContext.spa
|
||||
if (renderContext.spa === undefined) {
|
||||
// TODO: Remove reading from renderContext.res in Nuxt3
|
||||
renderContext.spa = !this.SSR || req.spa || res.spa
|
||||
}
|
||||
|
||||
// renderContext.modern
|
||||
if (renderContext.modern === undefined) {
|
||||
const modernMode = this.options.modern
|
||||
renderContext.modern = modernMode === 'client' || isModernRequest(req, modernMode)
|
||||
}
|
||||
|
||||
// Set runtime config on renderContext
|
||||
renderContext.runtimeConfig = {
|
||||
private: renderContext.spa ? {} : { ...this.options.privateRuntimeConfig },
|
||||
public: { ...this.options.publicRuntimeConfig }
|
||||
}
|
||||
|
||||
// Call renderContext hook
|
||||
await this.serverContext.nuxt.callHook('vue-renderer:context', renderContext)
|
||||
|
||||
// Render SPA or SSR
|
||||
return renderContext.spa
|
||||
? this.renderSPA(renderContext)
|
||||
: this.renderSSR(renderContext)
|
||||
}
|
||||
|
||||
get resourceMap () {
|
||||
return {
|
||||
clientManifest: {
|
||||
fileName: 'client.manifest.json',
|
||||
transform: src => JSON.parse(src)
|
||||
},
|
||||
modernManifest: {
|
||||
fileName: 'modern.manifest.json',
|
||||
transform: src => JSON.parse(src)
|
||||
},
|
||||
serverManifest: {
|
||||
fileName: 'server.manifest.json',
|
||||
// BundleRenderer needs resolved contents
|
||||
transform: async (src, { readResource }) => {
|
||||
const serverManifest = JSON.parse(src)
|
||||
|
||||
const readResources = async (obj) => {
|
||||
const _obj = {}
|
||||
await Promise.all(Object.keys(obj).map(async (key) => {
|
||||
_obj[key] = await readResource(obj[key])
|
||||
}))
|
||||
return _obj
|
||||
}
|
||||
|
||||
const [files, maps] = await Promise.all([
|
||||
readResources(serverManifest.files),
|
||||
readResources(serverManifest.maps)
|
||||
])
|
||||
|
||||
// Try to parse sourcemaps
|
||||
for (const map in maps) {
|
||||
if (maps[map] && maps[map].version) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
maps[map] = JSON.parse(maps[map])
|
||||
} catch (e) {
|
||||
maps[map] = { version: 3, sources: [], mappings: '' }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...serverManifest,
|
||||
files,
|
||||
maps
|
||||
}
|
||||
}
|
||||
},
|
||||
ssrTemplate: {
|
||||
fileName: 'index.ssr.html',
|
||||
transform: src => this.parseTemplate(src)
|
||||
},
|
||||
spaTemplate: {
|
||||
fileName: 'index.spa.html',
|
||||
transform: src => this.parseTemplate(src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseTemplate (templateStr) {
|
||||
return template(templateStr, {
|
||||
interpolate: /{{([\s\S]+?)}}/g,
|
||||
evaluate: /{%([\s\S]+?)%}/g
|
||||
})
|
||||
}
|
||||
|
||||
close () {
|
||||
if (this.__closed) {
|
||||
return
|
||||
}
|
||||
this.__closed = true
|
||||
|
||||
for (const key in this.renderer) {
|
||||
delete this.renderer[key]
|
||||
}
|
||||
}
|
||||
}
|
19
packages/nuxt3/src/vue-renderer/renderers/base.ts
Normal file
19
packages/nuxt3/src/vue-renderer/renderers/base.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export default class BaseRenderer {
|
||||
constructor (serverContext) {
|
||||
this.serverContext = serverContext
|
||||
this.options = serverContext.options
|
||||
}
|
||||
|
||||
renderTemplate (templateFn, opts) {
|
||||
// Fix problem with HTMLPlugin's minify option (#3392)
|
||||
opts.html_attrs = opts.HTML_ATTRS
|
||||
opts.head_attrs = opts.HEAD_ATTRS
|
||||
opts.body_attrs = opts.BODY_ATTRS
|
||||
|
||||
return templateFn(opts)
|
||||
}
|
||||
|
||||
render () {
|
||||
throw new Error('`render()` needs to be implemented')
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user