initial commit

This commit is contained in:
pooya parsa 2020-07-02 15:02:35 +02:00
commit 14f187e69b
123 changed files with 10034 additions and 0 deletions

View 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
}
}

View 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)
})
}
}
}
}

View 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
}

View 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
}
}

View 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
}
}
}

View 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()
}
}

View 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()
}

View 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
}
}

View 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) + '`')
}
}
}

View 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 })
}
}

View 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')
}
}

View 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')
}
}
}

View 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()
}
}

View 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)
}

View 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)
}
}

View 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)
}
}

View 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)
}

View 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'

View 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`))
}

View 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'
}
}

View File

@ -0,0 +1,9 @@
import common from './common'
import server from './server'
import locking from './locking'
export {
common,
server,
locking
}

View File

@ -0,0 +1,7 @@
export default {
lock: {
type: 'boolean',
default: true,
description: 'Do not set a lock on the project when building'
}
}

View 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'
}
}

View 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}`)
}
}

View 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()
}

View 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')))
}

View 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
}

View 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

View 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'))
}

View 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)
}

View 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())
}

View 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)
}

View 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
}
})

View 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: {}
})

View 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'
}
})

View File

@ -0,0 +1,4 @@
export default () => ({
badgeMessages: [],
bannerColor: 'green'
})

View 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}"
}
})

View 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()
}
}

View 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.'
})

View 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
}
}
})

View 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
}
}
}
})

View 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
})

View 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
})

View File

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

View 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))
}
}

View 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
}

View 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'

View 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
}

View 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
}
}

View 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()
}
}

View 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
}
}

View 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)
}
}

View File

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

View 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
}
}

View File

@ -0,0 +1,2 @@
export { default as Server } from './server'
export { default as Listener } from './listener'

View 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
}

View 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
}
}

View 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')
}
}

View 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 }
}

View 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}`
}
}

View File

@ -0,0 +1,6 @@
export default {
build: true,
rollup: {
externals: ['jsdom']
}
}

View 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]
}
}
}

View 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'))
}

View File

@ -0,0 +1,9 @@
export const TARGETS = {
server: 'server',
static: 'static'
}
export const MODES = {
universal: 'universal',
spa: 'spa'
}

View 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
}

View 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'

View File

@ -0,0 +1,44 @@
export const encodeHtml = function encodeHtml (str) {
return str.replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
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
}

View 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)
}
}
}
}

View 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()}}();'

View 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
}

View 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
}

View 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

View 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
}
}

View 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()
}
}

View 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>

View File

@ -0,0 +1 @@
// nothing here

View File

@ -0,0 +1 @@
export { init } from './nuxt'

View 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)
}
}

View 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
})

View 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
}

View File

@ -0,0 +1,3 @@
<template>
<Nuxt />
</template>

View File

@ -0,0 +1,5 @@
import sharedPlugins from './plugins'
export default [
...sharedPlugins
]

View 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
]

View File

@ -0,0 +1,7 @@
import sharedPlugins from './plugins'
import preload from 'nuxt-app/plugins/preload'
export default [
...sharedPlugins,
preload
]

View 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
}
]

View 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>

View 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>

View 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
}

View 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
}
}

View 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)
}
})
}

View 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()
})
}
}

View 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__ || {}
}
}

View 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
}

View File

@ -0,0 +1,3 @@
export function defineGetter (obj, key, val) {
Object.defineProperty(obj, key, { get: () => val })
}

View 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."
}
}

View 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."
}
}

View File

@ -0,0 +1 @@
export { default as VueRenderer } from './renderer'

View 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]
}
}
}

View 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