mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 00:23:53 +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