refactor core into sub-packages (#4202)

This commit is contained in:
Pooya Parsa 2018-10-31 00:12:53 +03:30 committed by GitHub
parent 4503d42d54
commit 39b558f59c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 1286 additions and 1097 deletions

View File

@ -26,26 +26,22 @@ jobs:
name: Install Dependencies
command: yarn --frozen-lockfile --non-interactive
# Link
- run:
name: Link
command: yarn lerna link
# Save cache
- save_cache:
key: yarn-{{ checksum "yarn.lock" }}
paths:
- node_modules
- distributions/*/node_modules
- packages/*/node_modules
- distributions/*/node_modules
# Persist workspace
- persist_to_workspace:
root: ~/project
paths:
- node_modules
- distributions/*/node_modules
- packages/*/node_modules
- distributions/*/node_modules
- packages/*/dist
# --------------------------------------------------------------------------
# Phase 2: Lint + Audit + Build Nuxt and fixtures
@ -76,18 +72,13 @@ jobs:
- checkout
- attach_workspace:
at: ~/project
- run:
name: Build Nuxt
command: yarn build
- run:
name: Build Fixtures
command: yarn build && yarn test:fixtures -w=4 --coverage && yarn coverage
command: yarn test:fixtures -w=4 --coverage && yarn coverage
- persist_to_workspace:
root: ~/project
paths:
- test/fixtures # TODO
- distributions/**/dist
- packages/**/dist
- test/fixtures
# --------------------------------------------------------------------------
# Phase 3: Unit and E2E tests

View File

@ -1,8 +1,16 @@
app
!app/store.js
# Common
node_modules
dist
.nuxt
examples/coffeescript/pages/index.vue
!examples/storybook/.storybook
coverage
# Examples
## cofeescript
examples/coffeescript/pages/index.vue
# Packages
# vue-app
packages/vue-app/template
!packages/vue-app/template/store.js

View File

@ -19,10 +19,6 @@ steps:
yarn
displayName: 'Install dependencies'
- script: |
yarn build
displayName: 'Build Nuxt'
- script: |
yarn test:fixtures -w=2
displayName: 'Test: Build Fixtures'

View File

@ -15,20 +15,20 @@ test.before(async () => {
}
nuxt = new Nuxt(config)
await new Builder(nuxt).build()
await nuxt.listen(4000, 'localhost')
await nuxt.server.listen(4000, 'localhost')
}, 30000)
// Example of testing only generated html
test('Route / exits and render HTML', async (t) => {
const context = {}
const { html } = await nuxt.renderRoute('/', context)
const { html } = await nuxt.server.renderRoute('/', context)
t.true(html.includes('<h1 class="red">Hello world!</h1>'))
})
// Example of testing via dom checking
test('Route / exits and render HTML with CSS applied', async (t) => {
const context = {}
const { html } = await nuxt.renderRoute('/', context)
const { html } = await nuxt.server.renderRoute('/', context)
const { window } = new JSDOM(html).window
const element = window.document.querySelector('.red')
t.not(element, null)

View File

@ -5,8 +5,8 @@ const server = http.createServer(this.nuxt.renderer.app)
const io = socketIO(server)
export default function () {
// overwrite nuxt.listen()
this.nuxt.listen = (port, host) => new Promise(resolve => server.listen(port || 3000, host || 'localhost', resolve))
// overwrite nuxt.server.listen()
this.nuxt.server.listen = (port, host) => new Promise(resolve => server.listen(port || 3000, host || 'localhost', resolve))
// close this server on 'close' event
this.nuxt.hook('close', () => new Promise(server.close))

View File

@ -14,32 +14,29 @@ module.exports = {
coverageDirectory: './coverage',
collectCoverageFrom: [
'packages/*/src/**/*.js',
'packages/cli/bin/*'
'**/packages/*/src/**/*.js'
],
coveragePathIgnorePatterns: [
'node_modules',
'packages/app',
'packages/webpack/plugins/vue'
'node_modules/(?!(@nuxt|nuxt))',
'packages/webpack/src/config/plugins/vue'
],
testPathIgnorePatterns: [
'node_modules',
'node_modules/(?!(@nuxt|nuxt))',
'test/fixtures/.*/.*?/',
'examples/.*'
],
transformIgnorePatterns: [
'node_modules/(?!(@nuxt|nuxt))'
],
transform: {
'^.+\\.js$': 'babel-jest',
'.*\\.(vue)$': 'vue-jest'
},
transformIgnorePatterns: [
'/node_modules/',
'/dist/'
],
moduleFileExtensions: [
'js',
'json'

View File

@ -10,7 +10,7 @@
"clean:build": "rimraf distributions/*/dist packages/*/dist",
"clean:examples": "rimraf examples/*/dist examples/*/.nuxt",
"clean:test": "rimraf test/fixtures/*/dist test/fixtures/*/.nuxt*",
"dev": "yarn build --watch",
"dev": "node -r esm ./scripts/dev",
"coverage": "codecov",
"lint": "eslint --ext .js,.mjs,.vue .",
"lint:app": "eslint-multiplexer eslint --ignore-path packages/app/template/.eslintignore 'test/fixtures/!(missing-plugin)/.nuxt!(-dev)/**' | eslint-multiplexer -b",
@ -20,7 +20,7 @@
"test:e2e": "jest -i test/e2e",
"test:lint": "yarn lint",
"test:unit": "jest test/unit",
"postinstall": "lerna link && node -r esm ./scripts/dev"
"postinstall": "lerna link && yarn dev"
},
"devDependencies": {
"@babel/core": "^7.1.2",

View File

@ -8,8 +8,8 @@
],
"main": "dist/builder.js",
"dependencies": {
"@nuxt/app": "^2.2.0",
"@nuxt/common": "^2.2.0",
"@nuxt/vue-app": "^2.2.0",
"@nuxtjs/devalue": "^1.0.1",
"chokidar": "^2.0.4",
"consola": "^1.4.4",

View File

@ -20,8 +20,6 @@ import values from 'lodash/values'
import devalue from '@nuxtjs/devalue'
import {
Options,
BuildContext,
r,
wp,
wChunk,
@ -33,6 +31,8 @@ import {
isString
} from '@nuxt/common'
import BuildContext from './context'
const glob = pify(Glob)
export default class Builder {
@ -69,7 +69,7 @@ export default class Builder {
}
// Resolve template
this.template = this.options.build.template || '@nuxt/app'
this.template = this.options.build.template || '@nuxt/vue-app'
if (typeof this.template === 'string') {
this.template = this.nuxt.resolver.requireModule(this.template)
}
@ -476,16 +476,16 @@ export default class Builder {
consola.success('Nuxt files generated')
}
// TODO: remove ignore when generateConfig enabled again
async generateConfig() /* istanbul ignore next */ {
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'
)
}
// TODO: Uncomment when generateConfig enabled again
// async generateConfig() /* istanbul ignore next */ {
// 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'
// )
// }
watchClient() {
const src = this.options.srcDir

View File

@ -48,9 +48,9 @@ export default {
})
.then(() => oldInstance && oldInstance.nuxt.close())
// Start listening
.then(() => nuxt.listen())
.then(() => nuxt.server.listen())
// Show ready message first time, others will be shown through WebpackBar
.then(() => !oldInstance && nuxt.showReady(false))
.then(() => !oldInstance && nuxt.server.showReady(false))
.then(() => builder.watchServer())
// Handle errors
.catch(err => errorHandler(err, { builder, nuxt }))

View File

@ -45,8 +45,8 @@ export default {
}
}
return nuxt.listen().then(() => {
nuxt.showReady(false)
return nuxt.server.listen().then(() => {
nuxt.server.showReady(false)
})
}
}

View File

@ -4,7 +4,7 @@ import consola from 'consola'
import esm from 'esm'
import wrapAnsi from 'wrap-ansi'
import defaultsDeep from 'lodash/defaultsDeep'
import { server as nuxtServerConfig } from '@nuxt/config'
import { getDefaultNuxtConfig } from '@nuxt/config'
const _require = esm(module, {
cache: false,
@ -58,7 +58,7 @@ export async function loadNuxtConfig(argv) {
port: argv.port || undefined,
host: argv.hostname || undefined,
socket: argv['unix-socket'] || undefined
}, options.server || {}, nuxtServerConfig(process.env))
}, options.server || {}, getDefaultNuxtConfig().server)
return options
}
@ -72,11 +72,11 @@ export function indentLines(string, spaces, firstLineSpaces) {
let s = ''
if (lines.length) {
const i0 = indent(firstLineSpaces === undefined ? spaces : firstLineSpaces)
s = i0 + lines.shift()
s = i0 + lines.shift().trim()
}
if (lines.length) {
const i = indent(spaces)
s += '\n' + lines.map(l => i + l).join('\n')
s += '\n' + lines.map(l => i + l.trim()).join('\n')
}
return s
}

View File

@ -3,7 +3,7 @@
exports[`cli/command builds help text 1`] = `
" Usage: nuxt this is how you do it [options]
a very long description that is longer than 80 chars and should wrap to the next
a very long description that is longer than 80 chars and should wrap to the next
line while keeping indentation
Options:
@ -16,7 +16,7 @@ exports[`cli/command builds help text 1`] = `
--port, -p Port number on which to start the application
--hostname, -H Hostname on which to start the application
--unix-socket, -n Path to a UNIX socket
--foo very long option that is longer than 80 chars and should wrap
--foo very long option that is longer than 80 chars and should wrap
to the next line while keeping indentation
"

View File

@ -22,8 +22,8 @@ describe('dev', () => {
expect(consola.error).not.toHaveBeenCalled()
expect(Builder.prototype.build).toHaveBeenCalled()
expect(Nuxt.prototype.listen).toHaveBeenCalled()
expect(Nuxt.prototype.showReady).toHaveBeenCalled()
expect(Nuxt.prototype.server.listen).toHaveBeenCalled()
expect(Nuxt.prototype.server.showReady).toHaveBeenCalled()
expect(Builder.prototype.watchServer).toHaveBeenCalled()
jest.clearAllMocks()
@ -37,8 +37,8 @@ describe('dev', () => {
expect(Builder.prototype.unwatch).toHaveBeenCalled()
expect(Builder.prototype.build).toHaveBeenCalled()
expect(Nuxt.prototype.close).toHaveBeenCalled()
expect(Nuxt.prototype.listen).toHaveBeenCalled()
expect(Nuxt.prototype.showReady).not.toHaveBeenCalled()
expect(Nuxt.prototype.server.listen).toHaveBeenCalled()
expect(Nuxt.prototype.server.showReady).not.toHaveBeenCalled()
expect(Builder.prototype.watchServer).toHaveBeenCalled()
expect(consola.error).not.toHaveBeenCalled()
@ -97,9 +97,11 @@ describe('dev', () => {
test('catches error on startDev', async () => {
mockNuxt({
listen: jest.fn().mockImplementation(() => {
throw new Error('Listen Error')
})
server: {
listen: jest.fn().mockImplementation(() => {
throw new Error('Listen Error')
})
}
})
mockBuilder()

View File

@ -1,4 +1,4 @@
import { server as nuxtServerConfig } from '@nuxt/config'
import { getDefaultNuxtConfig } from '@nuxt/config'
import { consola } from '../utils'
import * as utils from '../../src/utils'
@ -81,14 +81,14 @@ describe('cli/utils', () => {
})
test('nuxtServerConfig: server env', () => {
const options = {
server: nuxtServerConfig({
const options = getDefaultNuxtConfig({
env: {
...process.env,
HOST: 'env-host',
PORT: 3003,
UNIX_SOCKET: '/var/run/env.sock'
})
}
}
})
expect(options.server.host).toBe('env-host')
expect(options.server.port).toBe(3003)

View File

@ -66,8 +66,10 @@ export const mockGetNuxtStart = (ssr) => {
ssr
}
}, {
listen,
showReady
server: {
listen,
showReady
}
})
return { listen, showReady }
@ -89,8 +91,10 @@ export const mockNuxt = (implementation) => {
},
clearHook: jest.fn(),
close: jest.fn(),
listen: jest.fn().mockImplementationOnce(() => Promise.resolve()),
showReady: jest.fn().mockImplementationOnce(() => Promise.resolve())
server: {
listen: jest.fn().mockImplementationOnce(() => Promise.resolve()),
showReady: jest.fn().mockImplementationOnce(() => Promise.resolve())
}
}, implementation || {})
imports.core.mockImplementation(() => ({ Nuxt }))

View File

@ -0,0 +1,67 @@
import consola from 'consola'
import { sequence } from './utils'
export default class Hookable {
constructor() {
this._hooks = {}
this._deprecatedHooks = {}
this.hook = this.hook.bind(this)
this.callHook = this.callHook.bind(this)
}
hook(name, fn) {
if (!name || typeof fn !== 'function') {
return
}
if (this._deprecatedHooks[name]) {
consola.warn(`${name} hook has been deprecated, please use ${this._deprecatedHooks[name]}`)
name = this._deprecatedHooks[name]
}
this._hooks[name] = this._hooks[name] || []
this._hooks[name].push(fn)
}
async callHook(name, ...args) {
if (!this._hooks[name]) {
return
}
consola.debug(`Call ${name} hooks (${this._hooks[name].length})`)
try {
await sequence(this._hooks[name], fn => fn(...args))
} catch (err) {
consola.error(err)
this.callHook('error', err)
}
}
clearHook(name) {
if (name) {
delete this._hooks[name]
}
}
flatHooks(configHooks, hooks = {}, parentName) {
Object.keys(configHooks).forEach((key) => {
const subHook = configHooks[key]
const name = parentName ? `${parentName}:${key}` : key
if (typeof subHook === 'object' && subHook !== null) {
this.flatHooks(subHook, hooks, name)
} else {
hooks[name] = subHook
}
})
return hooks
}
addHooks(configHooks) {
const hooks = this.flatHooks(configHooks)
Object.keys(hooks).filter(Boolean).forEach((key) => {
[].concat(hooks[key]).forEach(h => this.hook(key, h))
})
}
}

View File

@ -1,3 +1,2 @@
export { default as Options } from './options'
export { default as BuildContext } from './build/context'
export { default as Hookable } from './hookable'
export * from './utils'

View File

@ -15,9 +15,10 @@ export const waitFor = function waitFor(ms) {
return new Promise(resolve => setTimeout(resolve, ms || 0))
}
export const isString = function isString(obj) {
return typeof obj === 'string' || obj instanceof String
}
export const isString = obj => typeof obj === 'string' || obj instanceof String
export const isNonEmptyString = obj => obj && isString(obj)
export const startsWithAlias = aliasArray => str => aliasArray.some(c => str.startsWith(c))
export const startsWithSrcAlias = startsWithAlias(['@', '~'])
@ -404,3 +405,34 @@ export const stripWhitespace = function stripWhitespace(string) {
})
return string
}
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
}
})
}

View File

@ -8,6 +8,7 @@
],
"main": "dist/config.js",
"dependencies": {
"@nuxt/common": "^2.2.0",
"consola": "^1.4.4",
"lodash": "^4.17.11",
"std-env": "^2.0.2"

View File

@ -0,0 +1,52 @@
export default () => ({
vue: {
config: {
silent: undefined, // = !dev
performance: undefined // = dev
}
},
head: {
meta: [],
link: [],
style: [],
script: []
},
plugins: [],
css: [],
modules: [],
layouts: {},
ErrorPage: null,
loading: {
color: 'black',
failedColor: 'red',
height: '2px',
throttle: 200,
duration: 5000,
continuous: false,
rtl: false,
css: true
},
loadingIndicator: 'default',
transition: {
name: 'page',
mode: 'out-in',
appear: false,
appearClass: 'appear',
appearActiveClass: 'appear-active',
appearToClass: 'appear-to'
},
layoutTransition: {
name: 'layout',
mode: 'out-in'
}
})

View File

@ -0,0 +1,80 @@
import path from 'path'
import fs from 'fs'
import capitalize from 'lodash/capitalize'
import env from 'std-env'
export default () => ({
// Env
dev: Boolean(env.dev),
test: Boolean(env.test),
debug: undefined, // = dev
env: {},
// Mode
mode: 'universal',
// Globals
globalName: `nuxt`,
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
srcDir: undefined,
buildDir: '.nuxt',
nuxtDir: fs.existsSync(path.resolve(__dirname, '..', '..', 'package.js'))
? path.resolve(__dirname, '..', '..') // src
: path.resolve(__dirname, '..'), // dist
modulesDir: [
'node_modules'
],
dir: {
assets: 'assets',
layouts: 'layouts',
middleware: 'middleware',
pages: 'pages',
static: 'static',
store: 'store'
},
extensions: [],
// Ignores
ignorePrefix: '-',
ignore: [
'**/*.test.*',
'**/*.spec.*'
],
// Generate
generate: {
dir: 'dist',
routes: [],
concurrency: 500,
interval: 0,
subFolders: true,
fallback: '200.html'
},
// Watch
watch: [],
watchers: {
webpack: {},
chokidar: {
ignoreInitial: true
}
},
// Editor
editor: undefined,
// Hooks
hooks: null
})

View File

@ -1,6 +1,6 @@
import env from 'std-env'
export default {
export default () => ({
quiet: Boolean(env.ci || env.test),
analyze: false,
profile: process.argv.includes('--profile'),
@ -109,4 +109,4 @@ export default {
/vue-ssr-client-manifest.json/
]
}
}
})

View File

@ -1,130 +1,27 @@
import path from 'path'
import fs from 'fs'
import capitalize from 'lodash/capitalize'
import env from 'std-env'
import render from './render'
import _app from './_app'
import _common from './_common'
import build from './build'
import router from './router'
import messages from './messages'
import modes from './modes'
import render from './render'
import router from './router'
import server from './server'
const nuxtDir = fs.existsSync(path.resolve(__dirname, '..', '..', 'package.js'))
? path.resolve(__dirname, '..', '..') // src
: path.resolve(__dirname, '..') // dist
export function getDefaultNuxtConfig(options = {}) {
if (!options.env) {
options.env = process.env
}
export default {
// Information about running environment
dev: Boolean(env.dev),
test: Boolean(env.test),
debug: undefined, // = dev
// Mode
mode: 'universal',
// Global name
globalName: `nuxt`,
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`
},
render,
build,
router,
messages,
// Server options
server: server(process.env),
// Dirs
srcDir: undefined,
buildDir: '.nuxt',
nuxtDir,
modulesDir: [
'node_modules'
],
// Ignore
ignorePrefix: '-',
ignore: [
'**/*.test.*',
'**/*.spec.*'
],
extensions: [],
generate: {
dir: 'dist',
routes: [],
concurrency: 500,
interval: 0,
subFolders: true,
fallback: '200.html'
},
env: {},
head: {
meta: [],
link: [],
style: [],
script: []
},
plugins: [],
css: [],
modules: [],
layouts: {},
serverMiddleware: [],
ErrorPage: null,
loading: {
color: 'black',
failedColor: 'red',
height: '2px',
throttle: 200,
duration: 5000,
continuous: false,
rtl: false,
css: true
},
loadingIndicator: 'default',
transition: {
name: 'page',
mode: 'out-in',
appear: false,
appearClass: 'appear',
appearActiveClass: 'appear-active',
appearToClass: 'appear-to'
},
layoutTransition: {
name: 'layout',
mode: 'out-in'
},
dir: {
assets: 'assets',
layouts: 'layouts',
middleware: 'middleware',
pages: 'pages',
static: 'static',
store: 'store'
},
vue: {
config: {
silent: undefined, // = !dev
performance: undefined // = dev
}
},
// User-defined changes
watch: [],
watchers: {
webpack: {},
chokidar: {
ignoreInitial: true
}
},
editor: undefined,
hooks: null
return {
..._app(options),
..._common(options),
build: build(options),
messages: messages(options),
modes: modes(options),
render: render(options),
router: router(options),
server: server(options)
}
}

View File

@ -1,4 +1,4 @@
export default {
export default () => ({
loading: 'Loading...',
error_404: 'This page could not be found',
server_error: 'Server error',
@ -9,4 +9,4 @@ export default {
client_error: 'Error',
client_error_details:
'An error occurred while rendering the page. Check developer tools console for details.'
}
})

View File

@ -1,4 +1,4 @@
export default {
export default () => ({
universal: {
build: {
ssr: true
@ -15,4 +15,4 @@ export default {
ssr: false
}
}
}
})

View File

@ -1,4 +1,4 @@
export default {
export default () => ({
bundleRenderer: {
shouldPrefetch: () => false
},
@ -24,4 +24,4 @@ export default {
// 1 year in production
maxAge: '1y'
}
}
})

View File

@ -1,4 +1,4 @@
export default {
export default () => ({
mode: 'history',
base: '/',
routes: [],
@ -10,4 +10,4 @@ export default {
parseQuery: false,
stringifyQuery: false,
fallback: false
}
})

View File

@ -1,4 +1,4 @@
export default env => ({
export default ({ env }) => ({
https: false,
port: env.NUXT_PORT ||
env.PORT ||

View File

@ -1,9 +1,2 @@
// Export individual bundles for easier access
export { default as Modes } from './modes'
export { default as build } from './config/build'
export { default as messages } from './config/messages'
export { default as render } from './config/render'
export { default as router } from './config/router'
export { default as server } from './config/server'
export { default as NuxtConfig } from './config'
export { getDefaultNuxtConfig } from './config'
export { getNuxtConfig } from './options'

View File

@ -5,17 +5,10 @@ import defaults from 'lodash/defaults'
import pick from 'lodash/pick'
import isObject from 'lodash/isObject'
import consola from 'consola'
import { NuxtConfig, Modes } from '@nuxt/config'
import { isPureObject, isUrl, guardDir, isString } from './utils'
import { isPureObject, isUrl, guardDir, isNonEmptyString } from '@nuxt/common'
import { getDefaultNuxtConfig } from './config'
// hasValue utility
const hasValue = v => typeof v === 'string' && v
const Options = {}
export default Options
Options.from = function (_options) {
export function getNuxtConfig(_options) {
// Clone options to prevent unwanted side-effects
const options = Object.assign({}, _options)
@ -43,12 +36,12 @@ Options.from = function (_options) {
options.extensions = [options.extensions]
}
options.globalName = (isString(options.globalName) && /^[a-zA-Z]+$/.test(options.globalName))
options.globalName = (isNonEmptyString(options.globalName) && /^[a-zA-Z]+$/.test(options.globalName))
? options.globalName.toLowerCase()
: `nuxt`
// Resolve rootDir
options.rootDir = hasValue(options.rootDir) ? path.resolve(options.rootDir) : process.cwd()
options.rootDir = isNonEmptyString(options.rootDir) ? path.resolve(options.rootDir) : process.cwd()
// Apply defaults by ${buildDir}/dist/build.config.js
// TODO: Unsafe operation.
@ -59,11 +52,13 @@ Options.from = function (_options) {
// }
// Apply defaults
defaultsDeep(options, NuxtConfig)
const nuxtConfig = getDefaultNuxtConfig()
nuxtConfig.build._publicPath = nuxtConfig.build.publicPath
defaultsDeep(options, nuxtConfig)
// Check srcDir and generate.dir excistence
const hasSrcDir = hasValue(options.srcDir)
const hasGenerateDir = hasValue(options.generate.dir)
const hasSrcDir = isNonEmptyString(options.srcDir)
const hasGenerateDir = isNonEmptyString(options.generate.dir)
// Resolve srcDir
options.srcDir = hasSrcDir
@ -97,7 +92,7 @@ Options.from = function (_options) {
// Populate modulesDir
options.modulesDir = []
.concat(options.modulesDir)
.concat(path.join(options.nuxtDir, 'node_modules')).filter(hasValue)
.concat(path.join(options.nuxtDir, 'node_modules')).filter(isNonEmptyString)
.map(dir => path.resolve(options.rootDir, dir))
const mandatoryExtensions = ['js', 'mjs']
@ -119,7 +114,7 @@ Options.from = function (_options) {
// Ignore publicPath on dev
/* istanbul ignore if */
if (options.dev && isUrl(options.build.publicPath)) {
options.build.publicPath = NuxtConfig.build.publicPath
options.build.publicPath = options.build._publicPath
}
// If store defined, update store options to true unless explicitly disabled
@ -218,7 +213,7 @@ Options.from = function (_options) {
}
// Apply mode preset
const modePreset = Modes[options.mode || 'universal'] || Modes.universal
const modePreset = options.modes[options.mode || 'universal']
defaultsDeep(options, modePreset)
// If no server-side rendering, add appear true transition

View File

@ -10,33 +10,17 @@
"dependencies": {
"@nuxt/common": "^2.2.0",
"@nuxt/config": "^2.2.0",
"@nuxt/server": "^2.2.0",
"@nuxt/vue-renderer": "^2.2.0",
"@nuxtjs/devalue": "^1.0.1",
"@nuxtjs/opencollective": "^0.1.0",
"@nuxtjs/youch": "^4.2.3",
"chalk": "^2.4.1",
"compression": "^1.7.3",
"connect": "^3.6.6",
"consola": "^1.4.4",
"debug": "^4.1.0",
"esm": "^3.0.84",
"etag": "^1.8.1",
"fresh": "^0.5.2",
"fs-extra": "^7.0.0",
"hash-sum": "^1.0.2",
"ip": "^1.1.5",
"launch-editor-middleware": "^2.2.1",
"lodash": "^4.17.11",
"lru-cache": "^4.1.3",
"serve-static": "^1.13.2",
"server-destroy": "^1.0.1",
"std-env": "^2.0.2",
"vue": "^2.5.17",
"vue-meta": "^1.5.5",
"vue-no-ssr": "^1.0.0",
"vue-router": "^3.0.1",
"vue-server-renderer": "^2.5.17",
"vue-template-compiler": "^2.5.17",
"vuex": "^3.0.1"
"std-env": "^2.0.2"
},
"publishConfig": {
"access": "public"

View File

@ -1,3 +1,3 @@
export { default as Module } from './module'
export { default as Nuxt } from './nuxt'
export { default as Renderer } from './renderer'
export { default as Resolver } from './resolver'

View File

@ -1,43 +1,40 @@
import https from 'https'
import enableDestroy from 'server-destroy'
import isPlainObject from 'lodash/isPlainObject'
import consola from 'consola'
import chalk from 'chalk'
import ip from 'ip'
import { Options, sequence } from '@nuxt/common'
import { Hookable, defineAlias } from '@nuxt/common'
import { getNuxtConfig } from '@nuxt/config'
import { Server } from '@nuxt/server'
import { version } from '../package.json'
import ModuleContainer from './module'
import Renderer from './renderer'
import Resolver from './resolver'
export default class Nuxt {
export default class Nuxt extends Hookable {
constructor(options = {}) {
this.options = Options.from(options)
super()
this.readyMessage = null
this.initialized = false
// Hooks
this._hooks = {}
this.hook = this.hook.bind(this)
// Assign options and apply defaults
this.options = getNuxtConfig(options)
// Create instance of core components
this.moduleContainer = new ModuleContainer(this)
this.renderer = new Renderer(this)
this.resolver = new Resolver(this)
this.moduleContainer = new ModuleContainer(this)
this.server = new Server(this)
// Backward compatibility
this.render = this.renderer.app
this.renderRoute = this.renderer.renderRoute.bind(this.renderer)
this.renderAndGetWindow = this.renderer.renderAndGetWindow.bind(
this.renderer
)
this.resolveAlias = this.resolver.resolveAlias.bind(this)
this.resolvePath = this.resolver.resolvePath.bind(this)
// Deprecated hooks
this._deprecatedHooks = {
'render:context': 'render:routeContext' // #3773
}
// Add Legacy aliases
this.renderer = this.server
this.render = this.server.app
defineAlias(this, this.server, [ 'renderRoute', 'renderAndGetWindow', 'showReady', 'listen' ])
defineAlias(this, this.resolver, [ 'resolveAlias', 'resolvePath' ])
// Wait for Nuxt to be ready
this.initialized = false
this._ready = this.ready().catch((err) => {
consola.fatal(err)
})
@ -62,8 +59,8 @@ export default class Nuxt {
// Await for modules
await this.moduleContainer.ready()
// Await for renderer to be ready
await this.renderer.ready()
// Await for server to be ready
await this.server.ready()
this.initialized = true
@ -73,156 +70,6 @@ export default class Nuxt {
return this
}
hook(name, fn) {
if (!name || typeof fn !== 'function') {
return
}
if (name === 'render:context') {
name = 'render:routeContext'
consola.warn('render:context hook has been deprecated, please use render:routeContext')
}
this._hooks[name] = this._hooks[name] || []
this._hooks[name].push(fn)
}
async callHook(name, ...args) {
if (!this._hooks[name]) {
return
}
consola.debug(`Call ${name} hooks (${this._hooks[name].length})`)
try {
await sequence(this._hooks[name], fn => fn(...args))
} catch (err) {
consola.error(err)
this.callHook('error', err)
}
}
clearHook(name) {
if (name) {
delete this._hooks[name]
}
}
flatHooks(configHooks, hooks = {}, parentName) {
Object.keys(configHooks).forEach((key) => {
const subHook = configHooks[key]
const name = parentName ? `${parentName}:${key}` : key
if (typeof subHook === 'object' && subHook !== null) {
this.flatHooks(subHook, hooks, name)
} else {
hooks[name] = subHook
}
})
return hooks
}
addHooks(configHooks) {
const hooks = this.flatHooks(configHooks)
Object.keys(hooks).filter(Boolean).forEach((key) => {
[].concat(hooks[key]).forEach(h => this.hook(key, h))
})
}
showReady(clear = true) {
if (!this.readyMessage) {
return
}
consola.ready({
message: this.readyMessage,
badge: true,
clear
})
this.readyMessage = null
}
listen(port, host, socket) {
return this.ready().then(() => new Promise((resolve, reject) => {
if (!socket && typeof this.options.server.socket === 'string') {
socket = this.options.server.socket
}
const args = { exclusive: false }
if (socket) {
args.path = socket
} else {
args.port = port || this.options.server.port
args.host = host || this.options.server.host
}
let appServer
const isHttps = Boolean(this.options.server.https)
if (isHttps) {
let httpsOptions
if (this.options.server.https === true) {
httpsOptions = {}
} else {
httpsOptions = this.options.server.https
}
appServer = https.createServer(httpsOptions, this.renderer.app)
} else {
appServer = this.renderer.app
}
const server = appServer.listen(
args,
(err) => {
/* istanbul ignore if */
if (err) {
return reject(err)
}
let listenURL
if (!socket) {
({ address: host, port } = server.address())
if (host === '127.0.0.1') {
host = 'localhost'
} else if (host === '0.0.0.0') {
host = ip.address()
}
listenURL = chalk.underline.blue(`http${isHttps ? 's' : ''}://${host}:${port}`)
this.readyMessage = `Listening on ${listenURL}`
} else {
listenURL = chalk.underline.blue(`unix+http://${socket}`)
this.readyMessage = `Listening on ${listenURL}`
}
// Close server on nuxt close
this.hook(
'close',
() =>
new Promise((resolve, reject) => {
// Destroy server by forcing every connection to be closed
server.listening && server.destroy((err) => {
consola.debug('server closed')
/* istanbul ignore if */
if (err) {
return reject(err)
}
resolve()
})
})
)
if (socket) {
this.callHook('listen', server, { path: socket }).then(resolve)
} else {
this.callHook('listen', server, { port, host }).then(resolve)
}
}
)
// Add server.destroy(cb) method
enableDestroy(server)
}))
}
async close(callback) {
await this.callHook('close', this)

View File

@ -1,484 +0,0 @@
import path from 'path'
import crypto from 'crypto'
import devalue from '@nuxtjs/devalue'
import serveStatic from 'serve-static'
import template from 'lodash/template'
import fs from 'fs-extra'
import { createBundleRenderer } from 'vue-server-renderer'
import connect from 'connect'
import launchMiddleware from 'launch-editor-middleware'
import consola from 'consola'
import { isUrl, timeout, waitFor, determineGlobals } from '@nuxt/common'
import { NuxtConfig } from '@nuxt/config'
import MetaRenderer from './meta'
import errorMiddleware from './middleware/error'
import nuxtMiddleware from './middleware/nuxt'
let jsdom = null
export default class Renderer {
constructor(nuxt) {
this.nuxt = nuxt
this.options = nuxt.options
this.globals = determineGlobals(nuxt.options.globalName, nuxt.options.globals)
// Will be set by createRenderer
this.bundleRenderer = null
this.metaRenderer = null
// Will be available on dev
this.webpackDevMiddleware = null
this.webpackHotMiddleware = null
// Create new connect instance
this.app = connect()
// Renderer runtime resources
this.resources = {
clientManifest: null,
serverBundle: null,
ssrTemplate: null,
spaTemplate: null,
errorTemplate: parseTemplate('Nuxt.js Internal Server Error')
}
}
async ready() {
await this.nuxt.callHook('render:before', this, this.options.render)
// Setup nuxt middleware
await this.setupMiddleware()
// Production: Load SSR resources from fs
if (!this.options.dev) {
await this.loadResources()
}
// Call done hook
await this.nuxt.callHook('render:done', this)
}
async loadResources(_fs = fs) {
const distPath = path.resolve(this.options.buildDir, 'dist', 'server')
const updated = []
resourceMap.forEach(({ key, fileName, transform }) => {
const rawKey = '$$' + key
const _path = path.join(distPath, fileName)
if (!_fs.existsSync(_path)) {
return // Resource not exists
}
const rawData = _fs.readFileSync(_path, 'utf8')
if (!rawData || rawData === this.resources[rawKey]) {
return // No changes
}
this.resources[rawKey] = rawData
const data = transform(rawData)
/* istanbul ignore if */
if (!data) {
return // Invalid data ?
}
this.resources[key] = data
updated.push(key)
})
// Reload error template
const errorTemplatePath = path.resolve(this.options.buildDir, 'views/error.html')
if (fs.existsSync(errorTemplatePath)) {
this.resources.errorTemplate = parseTemplate(
fs.readFileSync(errorTemplatePath, 'utf8')
)
}
// Load loading template
const loadingHTMLPath = path.resolve(this.options.buildDir, 'loading.html')
if (fs.existsSync(loadingHTMLPath)) {
this.resources.loadingHTML = fs.readFileSync(loadingHTMLPath, 'utf8')
this.resources.loadingHTML = this.resources.loadingHTML
.replace(/\r|\n|[\t\s]{3,}/g, '')
} else {
this.resources.loadingHTML = ''
}
// Call resourcesLoaded plugin
await this.nuxt.callHook('render:resourcesLoaded', this.resources)
if (updated.length > 0) {
this.createRenderer()
}
}
get noSSR() {
return this.options.render.ssr === false
}
get isReady() {
if (this.noSSR) {
return Boolean(this.resources.spaTemplate)
}
return Boolean(this.bundleRenderer && this.resources.ssrTemplate)
}
get isResourcesAvailable() {
// Required for both
/* istanbul ignore if */
if (!this.resources.clientManifest) {
return false
}
// Required for SPA rendering
if (this.noSSR) {
return Boolean(this.resources.spaTemplate)
}
// Required for bundle renderer
return Boolean(this.resources.ssrTemplate && this.resources.serverBundle)
}
createRenderer() {
// Ensure resources are available
if (!this.isResourcesAvailable) {
return
}
// Create Meta Renderer
this.metaRenderer = new MetaRenderer(this.nuxt, this)
// Skip following steps if noSSR mode
if (this.noSSR) {
return
}
const hasModules = fs.existsSync(path.resolve(this.options.rootDir, 'node_modules'))
// Create bundle renderer for SSR
this.bundleRenderer = createBundleRenderer(
this.resources.serverBundle,
Object.assign(
{
clientManifest: this.resources.clientManifest,
runInNewContext: false,
// for globally installed nuxt command, search dependencies in global dir
basedir: hasModules ? this.options.rootDir : __dirname
},
this.options.render.bundleRenderer
)
)
}
useMiddleware(m) {
// Resolve
const $m = m
if (typeof m === 'string') {
m = this.nuxt.resolver.requireModule(m)
}
if (typeof m.handler === 'string') {
m.handler = this.nuxt.resolver.requireModule(m.handler)
}
const handler = m.handler || m
const path = (
(m.prefix !== false ? this.options.router.base : '') +
(typeof m.path === 'string' ? m.path : '')
).replace(/\/\//g, '/')
handler.$m = $m
// Use middleware
this.app.use(path, handler)
}
get publicPath() {
return isUrl(this.options.build.publicPath)
? NuxtConfig.build.publicPath
: this.options.build.publicPath
}
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.compressor
if (typeof compressor === 'object') {
// If only setting for `compression` are provided, require the module and insert
// Prefer require instead of requireModule to keep dependency in nuxt-start
const compression = require('compression')
this.useMiddleware(compression(compressor))
} else {
// Else, require own compression middleware
this.useMiddleware(compressor)
}
}
// Add webpack middleware only for development
if (this.options.dev) {
this.useMiddleware(async (req, res, next) => {
if (this.webpackDevMiddleware) {
await this.webpackDevMiddleware(req, res)
}
if (this.webpackHotMiddleware) {
await this.webpackHotMiddleware(req, res)
}
next()
})
}
// open in editor for debug mode only
if (this.options.debug && this.options.dev) {
this.useMiddleware({
path: '__open-in-editor',
handler: launchMiddleware(this.options.editor)
})
}
// 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/ 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
)
})
}
// Add User provided middleware
this.options.serverMiddleware.forEach((m) => {
this.useMiddleware(m)
})
// Finally use nuxtMiddleware
this.useMiddleware(nuxtMiddleware.bind(this))
// Error middleware for errors that occurred in middleware that declared above
// Middleware should exactly take 4 arguments
// https://github.com/senchalabs/connect#error-middleware
// Apply errorMiddleware from modules first
await this.nuxt.callHook('render:errorMiddleware', this.app)
// Apply errorMiddleware from Nuxt
this.useMiddleware(errorMiddleware.bind(this))
}
renderTemplate(ssr, opts) {
// Fix problem with HTMLPlugin's minify option (#3392)
opts.html_attrs = opts.HTML_ATTRS
opts.body_attrs = opts.BODY_ATTRS
const fn = ssr ? this.resources.ssrTemplate : this.resources.spaTemplate
return fn(opts)
}
async renderRoute(url, context = {}) {
/* istanbul ignore if */
if (!this.isReady) {
await waitFor(1000)
return this.renderRoute(url, context)
}
// Log rendered url
consola.debug(`Rendering url ${url}`)
// Add url and isSever to the context
context.url = url
// Basic response if SSR is disabled or spa data provided
const spa = context.spa || (context.res && context.res.spa)
const ENV = this.options.env
if (this.noSSR || spa) {
const {
HTML_ATTRS,
BODY_ATTRS,
HEAD,
BODY_SCRIPTS,
getPreloadFiles
} = await this.metaRenderer.render(context)
const APP =
`<div id="${this.globals.id}">${this.resources.loadingHTML}</div>` + BODY_SCRIPTS
// Detect 404 errors
if (
url.includes(this.options.build.publicPath) ||
url.includes('__webpack')
) {
const err = {
statusCode: 404,
message: this.options.messages.error_404,
name: 'ResourceNotFound'
}
throw err
}
const html = this.renderTemplate(false, {
HTML_ATTRS,
BODY_ATTRS,
HEAD,
APP,
ENV
})
return { html, getPreloadFiles }
}
// Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
let APP = await this.bundleRenderer.renderToString(context)
if (!context.nuxt.serverRendered) {
APP = `<div id="${this.globals.id}"></div>`
}
const m = context.meta.inject()
let HEAD =
m.title.text() +
m.meta.text() +
m.link.text() +
m.style.text() +
m.script.text() +
m.noscript.text()
if (this.options._routerBaseSpecified) {
HEAD += `<base href="${this.options.router.base}">`
}
if (this.options.render.resourceHints) {
HEAD += context.renderResourceHints()
}
await this.nuxt.callHook('render:routeContext', context.nuxt)
const serializedSession = `window.${this.globals.context}=${devalue(context.nuxt)};`
const cspScriptSrcHashSet = new Set()
if (this.options.render.csp) {
const { hashAlgorithm } = this.options.render.csp
const hash = crypto.createHash(hashAlgorithm)
hash.update(serializedSession)
cspScriptSrcHashSet.add(`'${hashAlgorithm}-${hash.digest('base64')}'`)
}
APP += `<script>${serializedSession}</script>`
APP += context.renderScripts()
APP += m.script.text({ body: true })
APP += m.noscript.text({ body: true })
HEAD += context.renderStyles()
const html = this.renderTemplate(true, {
HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
BODY_ATTRS: m.bodyAttrs.text(),
HEAD,
APP,
ENV
})
return {
html,
cspScriptSrcHashSet,
getPreloadFiles: context.getPreloadFiles,
error: context.nuxt.error,
redirected: context.redirected
}
}
async renderAndGetWindow(url, opts = {}) {
/* istanbul ignore if */
if (!jsdom) {
try {
jsdom = require('jsdom')
} catch (e) /* istanbul ignore next */ {
consola.error(`
Fail when calling nuxt.renderAndGetWindow(url)
jsdom module is not installed
Please install jsdom with: npm install --save-dev jsdom
`)
throw e
}
}
const options = Object.assign({
resources: 'usable', // load subresources (https://github.com/tmpvar/jsdom#loading-subresources)
runScripts: 'dangerously',
virtualConsole: true,
beforeParse(window) {
// Mock window.scrollTo
window.scrollTo = () => {
}
}
}, opts)
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)
}
url = url || 'http://localhost:3000'
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(
this.options.render.ssr ? `window.${this.globals.context}` : `<div id="${this.globals.id}">`
)
/* istanbul ignore if */
if (!nuxtExists) {
const error = new Error('Could not load the nuxt app')
error.body = window.document.body.innerHTML
throw error
}
// Used by nuxt.js to say when the components are loaded and the app ready
const onNuxtLoaded = this.globals.loadedCallback
await timeout(new Promise((resolve) => {
window[onNuxtLoaded] = () => resolve(window)
}), 20000, 'Components loading in renderAndGetWindow was not completed in 20s')
if (options.virtualConsole) {
// after window initialized successfully
options.virtualConsole.removeListener('jsdomError', jsdomErrHandler)
}
// Send back window object
return window
}
}
const parseTemplate = templateStr =>
template(templateStr, {
interpolate: /{{([\s\S]+?)}}/g
})
export const resourceMap = [
{
key: 'clientManifest',
fileName: 'vue-ssr-client-manifest.json',
transform: JSON.parse
},
{
key: 'serverBundle',
fileName: 'server-bundle.json',
transform: JSON.parse
},
{
key: 'ssrTemplate',
fileName: 'index.ssr.html',
transform: parseTemplate
},
{
key: 'spaTemplate',
fileName: 'index.spa.html',
transform: parseTemplate
}
]

View File

@ -153,7 +153,7 @@ export default class Generator {
}
// Render and write the SPA template to the fallback path
const { html } = await this.nuxt.renderRoute('/', { spa: true })
const { html } = await this.nuxt.server.renderRoute('/', { spa: true })
await fsExtra.writeFile(fallbackPath, html, 'utf8')
}
@ -201,7 +201,7 @@ export default class Generator {
const pageErrors = []
try {
const res = await this.nuxt.renderer.renderRoute(route, {
const res = await this.nuxt.server.renderRoute(route, {
_generate: true,
payload
})

View File

@ -0,0 +1,29 @@
{
"name": "@nuxt/server",
"version": "2.2.0",
"repository": "nuxt/nuxt.js",
"license": "MIT",
"files": [
"dist"
],
"main": "dist/server.js",
"dependencies": {
"@nuxt/common": "^2.2.0",
"@nuxt/config": "^2.2.0",
"@nuxtjs/youch": "^4.2.3",
"chalk": "^2.4.1",
"compression": "^1.7.3",
"connect": "^3.6.6",
"consola": "^1.4.4",
"etag": "^1.8.1",
"fresh": "^0.5.2",
"fs-extra": "^7.0.0",
"ip": "^1.1.5",
"launch-editor-middleware": "^2.2.1",
"serve-static": "^1.13.2",
"server-destroy": "^1.0.1"
},
"publishConfig": {
"access": "public"
}
}

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 @@
export { default as Server } from './server'

View File

@ -0,0 +1,75 @@
import consola from 'consola'
import { timeout } from '@nuxt/common'
export default async function renderAndGetWindow(
url = 'http://localhost:3000',
jsdomOpts = {},
{
loadedCallback,
loadingTimeout = 2000,
ssr,
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(
ssr ? `window.${globals.context}` : `<div id="${globals.id}">`
)
/* istanbul ignore if */
if (!nuxtExists) {
const error = new Error('Could not load the nuxt app')
error.body = window.document.body.innerHTML
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 ${timeout / 1000}s`)
if (options.virtualConsole) {
// After window initialized successfully
options.virtualConsole.removeListener('jsdomError', jsdomErrHandler)
}
// Send back window object
return window
}

View File

@ -4,7 +4,7 @@ import consola from 'consola'
import Youch from '@nuxtjs/youch'
export default function errorMiddleware(err, req, res, next) {
export default ({ resources, options }) => function errorMiddleware(err, req, res, next) {
// ensure statusCode, message and name fields
err.statusCode = err.statusCode || 500
err.message = err.message || 'Nuxt Server Error'
@ -35,7 +35,7 @@ export default function errorMiddleware(err, req, res, next) {
hasReqHeader('user-agent', 'curl/')
// Use basic errors when debug mode is disabled
if (!this.options.debug) {
if (!options.debug) {
// Json format is compatible with Youch json responses
const json = {
status: err.statusCode,
@ -46,7 +46,7 @@ export default function errorMiddleware(err, req, res, next) {
sendResponse(JSON.stringify(json, undefined, 2), 'text/json')
return
}
const html = this.resources.errorTemplate(json)
const html = resources.errorTemplate(json)
sendResponse(html)
return
}
@ -55,8 +55,13 @@ export default function errorMiddleware(err, req, res, next) {
const youch = new Youch(
err,
req,
readSource.bind(this),
this.options.router.base,
readSourceFactory({
srcDir: options.srcDir,
rootDir: options.rootDir,
buildDir: options.buildDir,
resources
}),
options.router.base,
true
)
if (isJson) {
@ -68,7 +73,7 @@ export default function errorMiddleware(err, req, res, next) {
}
}
async function readSource(frame) {
const readSourceFactory = ({ srcDir, rootDir, buildDir, resources }) => async function readSource(frame) {
// Remove webpack:/// & query string from the end
const sanitizeName = name =>
name ? name.replace('webpack:///', '').split('?')[0] : null
@ -82,10 +87,10 @@ async function readSource(frame) {
// Possible paths for file
const searchPath = [
this.options.srcDir,
this.options.rootDir,
path.join(this.options.buildDir, 'dist', 'server'),
this.options.buildDir,
srcDir,
rootDir,
path.join(buildDir, 'dist', 'server'),
buildDir,
process.cwd()
]
@ -97,7 +102,7 @@ async function readSource(frame) {
frame.contents = source
frame.fullPath = fullPath
if (path.isAbsolute(frame.fileName)) {
frame.fileName = path.relative(this.options.rootDir, fullPath)
frame.fileName = path.relative(rootDir, fullPath)
}
return
}
@ -107,6 +112,6 @@ async function readSource(frame) {
// TODO: restore to if after https://github.com/istanbuljs/nyc/issues/595 fixed
/* istanbul ignore next */
if (!frame.contents) {
frame.contents = this.resources.serverBundle.files[frame.fileName]
frame.contents = resources.serverBundle.files[frame.fileName]
}
}

View File

@ -4,14 +4,14 @@ import consola from 'consola'
import { getContext } from '@nuxt/common'
export default async function nuxtMiddleware(req, res, next) {
export default ({ options, nuxt, renderRoute, resources }) => async function nuxtMiddleware(req, res, next) {
// Get context
const context = getContext(req, res)
res.statusCode = 200
try {
const result = await this.renderRoute(req.url, context)
await this.nuxt.callHook('render:route', req.url, result, context)
const result = await renderRoute(req.url, context)
await nuxt.callHook('render:route', req.url, result, context)
const {
html,
cspScriptSrcHashSet,
@ -21,7 +21,7 @@ export default async function nuxtMiddleware(req, res, next) {
} = result
if (redirected) {
this.nuxt.callHook('render:routeDone', req.url, result, context)
nuxt.callHook('render:routeDone', req.url, result, context)
return html
}
if (error) {
@ -29,26 +29,29 @@ export default async function nuxtMiddleware(req, res, next) {
}
// Add ETag header
if (!error && this.options.render.etag) {
const etag = generateETag(html, this.options.render.etag)
if (!error && options.render.etag) {
const etag = generateETag(html, options.render.etag)
if (fresh(req.headers, { etag })) {
res.statusCode = 304
res.end()
this.nuxt.callHook('render:routeDone', req.url, result, context)
nuxt.callHook('render:routeDone', req.url, result, context)
return
}
res.setHeader('ETag', etag)
}
// HTTP2 push headers for preload assets
if (!error && this.options.render.http2.push) {
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 preloadFiles = getPreloadFiles()
const { shouldPush, pushAssets } = this.options.render.http2
const { publicPath } = this.resources.clientManifest
const links = pushAssets ? pushAssets(req, res, publicPath, preloadFiles) : defaultPushAssets(preloadFiles, shouldPush, publicPath, this.options.dev)
const { shouldPush, pushAssets } = options.render.http2
const { publicPath } = resources.clientManifest
const links = pushAssets
? pushAssets(req, res, publicPath, preloadFiles)
: defaultPushAssets(preloadFiles, shouldPush, publicPath, options.dev)
// Pass with single Link header
// https://blog.cloudflare.com/http-2-server-push-with-multiple-assets-per-link-header
@ -58,18 +61,18 @@ export default async function nuxtMiddleware(req, res, next) {
}
}
if (this.options.render.csp) {
const { allowedSources, policies } = this.options.render.csp
const cspHeader = this.options.render.csp.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
if (options.render.csp) {
const { allowedSources, policies } = options.render.csp
const cspHeader = options.render.csp.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
res.setHeader(cspHeader, getCspString({ cspScriptSrcHashSet, allowedSources, policies, isDev: this.options.dev }))
res.setHeader(cspHeader, getCspString({ cspScriptSrcHashSet, allowedSources, policies, isDev: options.dev }))
}
// Send response
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.setHeader('Content-Length', Buffer.byteLength(html))
res.end(html, 'utf8')
this.nuxt.callHook('render:routeDone', req.url, result, context)
nuxt.callHook('render:routeDone', req.url, result, context)
return html
} catch (err) {
/* istanbul ignore if */

View File

@ -0,0 +1,276 @@
import https from 'https'
import path from 'path'
import enableDestroy from 'server-destroy'
import launchMiddleware from 'launch-editor-middleware'
import serveStatic from 'serve-static'
import chalk from 'chalk'
import ip from 'ip'
import consola from 'consola'
import connect from 'connect'
import { determineGlobals, isUrl } from '@nuxt/common'
import ServerContext from './context'
import renderAndGetWindow from './jsdom'
import nuxtMiddleware from './middleware/nuxt'
import errorMiddleware from './middleware/error'
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 available on dev
this.webpackDevMiddleware = null
this.webpackHotMiddleware = null
// Create new connect instance
this.app = connect()
}
async ready() {
await this.nuxt.callHook('render:before', this, this.options.render)
// Initialize vue-renderer
const { VueRenderer } = await import('@nuxt/vue-renderer')
const context = new ServerContext(this)
this.renderer = new VueRenderer(context)
await this.renderer.ready()
// Setup nuxt middleware
await this.setupMiddleware()
// Call done hook
await this.nuxt.callHook('render:done', 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.compressor
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 {
// Else, require own compression middleware
this.useMiddleware(compressor)
}
}
// Add webpack middleware support only for development
if (this.options.dev) {
this.useMiddleware(async (req, res, next) => {
if (this.webpackDevMiddleware) {
await this.webpackDevMiddleware(req, res)
}
if (this.webpackHotMiddleware) {
await this.webpackHotMiddleware(req, res)
}
next()
})
}
// open in editor for debug mode only
if (this.options.debug && this.options.dev) {
this.useMiddleware({
path: '__open-in-editor',
handler: launchMiddleware(this.options.editor)
})
}
// 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
)
})
}
// Add User provided middleware
this.options.serverMiddleware.forEach((m) => {
this.useMiddleware(m)
})
// Finally use nuxtMiddleware
this.useMiddleware(nuxtMiddleware({
options: this.options,
nuxt: this.nuxt,
renderRoute: this.renderRoute.bind(this),
resources: this.resources
}))
// Error middleware for errors that occurred in middleware that declared above
// Middleware should exactly take 4 arguments
// https://github.com/senchalabs/connect#error-middleware
// Apply errorMiddleware from modules first
await this.nuxt.callHook('render:errorMiddleware', this.app)
// Apply errorMiddleware from Nuxt
this.useMiddleware(errorMiddleware({
resources: this.resources,
options: this.options
}))
}
useMiddleware(middleware) {
// Resolve middleware
if (typeof middleware === 'string') {
middleware = this.nuxt.resolver.requireModule(middleware)
}
// Resolve handler
if (typeof middleware.handler === 'string') {
middleware.handler = this.nuxt.resolver.requireModule(middleware.handler)
}
const handler = middleware.handler || middleware
// Resolve path
const path = (
(middleware.prefix !== false ? this.options.router.base : '') +
(typeof middleware.path === 'string' ? middleware.path : '')
).replace(/\/\//g, '/')
// Use middleware
this.app.use(path, handler)
}
renderRoute() {
return this.renderer.renderRoute.apply(this.renderer, arguments)
}
loadResources() {
return this.renderer.loadResources.apply(this.renderer, arguments)
}
renderAndGetWindow(url, opts = {}) {
return renderAndGetWindow(url, opts, {
loadedCallback: this.globals.loadedCallback,
ssr: this.options.render.ssr,
globals: this.globals
})
}
showReady(clear = true) {
if (this.readyMessage) {
consola.ready({
message: this.readyMessage,
badge: true,
clear
})
}
}
listen(port, host, socket) {
return new Promise((resolve, reject) => {
if (!socket && typeof this.options.server.socket === 'string') {
socket = this.options.server.socket
}
const args = { exclusive: false }
if (socket) {
args.path = socket
} else {
args.port = port || this.options.server.port
args.host = host || this.options.server.host
}
let appServer
const isHttps = Boolean(this.options.server.https)
if (isHttps) {
let httpsOptions
if (this.options.server.https === true) {
httpsOptions = {}
} else {
httpsOptions = this.options.server.https
}
appServer = https.createServer(httpsOptions, this.app)
} else {
appServer = this.app
}
const server = appServer.listen(
args,
(err) => {
/* istanbul ignore if */
if (err) {
return reject(err)
}
let listenURL
if (!socket) {
({ address: host, port } = server.address())
if (host === '127.0.0.1') {
host = 'localhost'
} else if (host === '0.0.0.0') {
host = ip.address()
}
listenURL = chalk.underline.blue(`http${isHttps ? 's' : ''}://${host}:${port}`)
this.readyMessage = `Listening on ${listenURL}`
} else {
listenURL = chalk.underline.blue(`unix+http://${socket}`)
this.readyMessage = `Listening on ${listenURL}`
}
// Close server on nuxt close
this.nuxt.hook(
'close',
() =>
new Promise((resolve, reject) => {
// Destroy server by forcing every connection to be closed
server.listening && server.destroy((err) => {
consola.debug('server closed')
/* istanbul ignore if */
if (err) {
return reject(err)
}
resolve()
})
})
)
if (socket) {
this.nuxt.callHook('listen', server, { path: socket }).then(resolve)
} else {
this.nuxt.callHook('listen', server, { port, host }).then(resolve)
}
}
)
// Add server.destroy(cb) method
enableDestroy(server)
})
}
}

View File

@ -0,0 +1,3 @@
export default {
build: true
}

View File

@ -1,5 +1,5 @@
{
"name": "@nuxt/app",
"name": "@nuxt/vue-app",
"version": "2.2.0",
"repository": "nuxt/nuxt.js",
"license": "MIT",
@ -7,7 +7,7 @@
"dist",
"template"
],
"main": "dist/app.js",
"main": "dist/vue-app.js",
"publishConfig": {
"access": "public"
}

View File

@ -0,0 +1,3 @@
export default {
build: true
}

View File

@ -0,0 +1,27 @@
{
"name": "@nuxt/vue-renderer",
"version": "2.2.0",
"repository": "nuxt/nuxt.js",
"license": "MIT",
"files": [
"dist"
],
"main": "dist/vue-renderer.js",
"dependencies": {
"@nuxt/common": "^2.2.0",
"@nuxtjs/devalue": "^1.0.1",
"consola": "^1.4.4",
"fs-extra": "^7.0.0",
"lru-cache": "^4.1.3",
"vue": "^2.5.17",
"vue-meta": "^1.5.5",
"vue-no-ssr": "^1.0.0",
"vue-router": "^3.0.1",
"vue-server-renderer": "^2.5.17",
"vue-template-compiler": "^2.5.17",
"vuex": "^3.0.1"
},
"publishConfig": {
"access": "public"
}
}

View File

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

View File

@ -0,0 +1,297 @@
import path from 'path'
import crypto from 'crypto'
import devalue from '@nuxtjs/devalue'
import template from 'lodash/template'
import fs from 'fs-extra'
import { createBundleRenderer } from 'vue-server-renderer'
import consola from 'consola'
import { waitFor } from '@nuxt/common'
import SPAMetaRenderer from './spa-meta'
export default class VueRenderer {
constructor(context) {
this.context = context
// Will be set by createRenderer
this.bundleRenderer = null
this.spaMetaRenderer = null
// Renderer runtime resources
Object.assign(this.context.resources, {
clientManifest: null,
serverBundle: null,
ssrTemplate: null,
spaTemplate: null,
errorTemplate: this.constructor.parseTemplate('Nuxt.js Internal Server Error')
})
}
async ready() {
// Production: Load SSR resources from fs
if (!this.context.options.dev) {
await this.loadResources()
}
}
async loadResources(_fs = fs) {
const distPath = path.resolve(this.context.options.buildDir, 'dist', 'server')
const updated = []
this.constructor.resourceMap.forEach(({ key, fileName, transform }) => {
const rawKey = '$$' + key
const _path = path.join(distPath, fileName)
if (!_fs.existsSync(_path)) {
return // Resource not exists
}
const rawData = _fs.readFileSync(_path, 'utf8')
if (!rawData || rawData === this.context.resources[rawKey]) {
return // No changes
}
this.context.resources[rawKey] = rawData
const data = transform(rawData)
/* istanbul ignore if */
if (!data) {
return // Invalid data ?
}
this.context.resources[key] = data
updated.push(key)
})
// Reload error template
const errorTemplatePath = path.resolve(this.context.options.buildDir, 'views/error.html')
if (fs.existsSync(errorTemplatePath)) {
this.context.resources.errorTemplate = this.constructor.parseTemplate(
fs.readFileSync(errorTemplatePath, 'utf8')
)
}
// Load loading template
const loadingHTMLPath = path.resolve(this.context.options.buildDir, 'loading.html')
if (fs.existsSync(loadingHTMLPath)) {
this.context.resources.loadingHTML = fs.readFileSync(loadingHTMLPath, 'utf8')
this.context.resources.loadingHTML = this.context.resources.loadingHTML
.replace(/\r|\n|[\t\s]{3,}/g, '')
} else {
this.context.resources.loadingHTML = ''
}
// Call resourcesLoaded plugin
await this.context.nuxt.callHook('render:resourcesLoaded', this.context.resources)
if (updated.length > 0) {
this.createRenderer()
}
}
get noSSR() {
return this.context.options.render.ssr === false
}
get isReady() {
if (this.noSSR) {
return Boolean(this.context.resources.spaTemplate)
}
return Boolean(this.bundleRenderer && this.context.resources.ssrTemplate)
}
get isResourcesAvailable() {
// Required for both
/* istanbul ignore if */
if (!this.context.resources.clientManifest) {
return false
}
// Required for SPA rendering
if (this.noSSR) {
return Boolean(this.context.resources.spaTemplate)
}
// Required for bundle renderer
return Boolean(this.context.resources.ssrTemplate && this.context.resources.serverBundle)
}
createRenderer() {
// Ensure resources are available
if (!this.isResourcesAvailable) {
return
}
// Create Meta Renderer
this.spaMetaRenderer = new SPAMetaRenderer(this)
// Skip following steps if noSSR mode
if (this.noSSR) {
return
}
const hasModules = fs.existsSync(path.resolve(this.context.options.rootDir, 'node_modules'))
// Create bundle renderer for SSR
this.bundleRenderer = createBundleRenderer(
this.context.resources.serverBundle,
Object.assign(
{
clientManifest: this.context.resources.clientManifest,
runInNewContext: false,
// for globally installed nuxt command, search dependencies in global dir
basedir: hasModules ? this.context.options.rootDir : __dirname
},
this.context.options.render.bundleRenderer
)
)
}
renderTemplate(ssr, opts) {
// Fix problem with HTMLPlugin's minify option (#3392)
opts.html_attrs = opts.HTML_ATTRS
opts.body_attrs = opts.BODY_ATTRS
const fn = ssr ? this.context.resources.ssrTemplate : this.context.resources.spaTemplate
return fn(opts)
}
async renderRoute(url, context = {}) {
/* istanbul ignore if */
if (!this.isReady) {
await waitFor(1000)
return this.renderRoute(url, context)
}
// Log rendered url
consola.debug(`Rendering url ${url}`)
// Add url and isSever to the context
context.url = url
// Basic response if SSR is disabled or spa data provided
const spa = context.spa || (context.res && context.res.spa)
const ENV = this.context.options.env
if (this.noSSR || spa) {
const {
HTML_ATTRS,
BODY_ATTRS,
HEAD,
BODY_SCRIPTS,
getPreloadFiles
} = await this.spaMetaRenderer.render(context)
const APP =
`<div id="${this.context.globals.id}">${this.context.resources.loadingHTML}</div>` + BODY_SCRIPTS
// Detect 404 errors
if (
url.includes(this.context.options.build.publicPath) ||
url.includes('__webpack')
) {
const err = {
statusCode: 404,
message: this.context.options.messages.error_404,
name: 'ResourceNotFound'
}
throw err
}
const html = this.renderTemplate(false, {
HTML_ATTRS,
BODY_ATTRS,
HEAD,
APP,
ENV
})
return { html, getPreloadFiles }
}
// Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
let APP = await this.bundleRenderer.renderToString(context)
if (!context.nuxt.serverRendered) {
APP = `<div id="${this.context.globals.id}"></div>`
}
const m = context.meta.inject()
let HEAD =
m.title.text() +
m.meta.text() +
m.link.text() +
m.style.text() +
m.script.text() +
m.noscript.text()
if (this.context.options._routerBaseSpecified) {
HEAD += `<base href="${this.context.options.router.base}">`
}
if (this.context.options.render.resourceHints) {
HEAD += context.renderResourceHints()
}
await this.context.nuxt.callHook('render:routeContext', context.nuxt)
const serializedSession = `window.${this.context.globals.context}=${devalue(context.nuxt)};`
const cspScriptSrcHashSet = new Set()
if (this.context.options.render.csp) {
const { hashAlgorithm } = this.context.options.render.csp
const hash = crypto.createHash(hashAlgorithm)
hash.update(serializedSession)
cspScriptSrcHashSet.add(`'${hashAlgorithm}-${hash.digest('base64')}'`)
}
APP += `<script>${serializedSession}</script>`
APP += context.renderScripts()
APP += m.script.text({ body: true })
APP += m.noscript.text({ body: true })
HEAD += context.renderStyles()
const html = this.renderTemplate(true, {
HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
BODY_ATTRS: m.bodyAttrs.text(),
HEAD,
APP,
ENV
})
return {
html,
cspScriptSrcHashSet,
getPreloadFiles: context.getPreloadFiles,
error: context.nuxt.error,
redirected: context.redirected
}
}
static parseTemplate(templateStr) {
return template(templateStr, {
interpolate: /{{([\s\S]+?)}}/g
})
}
static get resourceMap() {
return [
{
key: 'clientManifest',
fileName: 'vue-ssr-client-manifest.json',
transform: JSON.parse
},
{
key: 'serverBundle',
fileName: 'server-bundle.json',
transform: JSON.parse
},
{
key: 'ssrTemplate',
fileName: 'index.ssr.html',
transform: this.parseTemplate
},
{
key: 'spaTemplate',
fileName: 'index.spa.html',
transform: this.parseTemplate
}
]
}
}

View File

@ -3,11 +3,10 @@ import VueMeta from 'vue-meta'
import { createRenderer } from 'vue-server-renderer'
import LRU from 'lru-cache'
export default class MetaRenderer {
constructor(nuxt, renderer) {
this.nuxt = nuxt
export default class SPAMetaRenderer {
constructor(renderer) {
this.renderer = renderer
this.options = nuxt.options
this.options = this.renderer.context.options
this.vueRenderer = createRenderer()
this.cache = LRU({})
@ -69,7 +68,7 @@ export default class MetaRenderer {
meta.resourceHints = ''
const clientManifest = this.renderer.resources.clientManifest
const clientManifest = this.renderer.context.resources.clientManifest
const shouldPreload = this.options.render.bundleRenderer.shouldPreload || (() => true)
const shouldPrefetch = this.options.render.bundleRenderer.shouldPrefetch || (() => true)

View File

@ -120,7 +120,7 @@ export class WebpackBuilder {
})
// Reload renderer if available
nuxt.renderer.loadResources(this.mfs || fs)
nuxt.server.loadResources(this.mfs || fs)
// Resolve on next tick
process.nextTick(resolve)
@ -166,7 +166,7 @@ export class WebpackBuilder {
webpackDev(compiler) {
consola.debug('Adding webpack middleware...')
const { nuxt: { renderer }, options } = this.context
const { nuxt: { server }, options } = this.context
// Create webpack dev middleware
this.webpackDevMiddleware = pify(
@ -200,9 +200,9 @@ export class WebpackBuilder {
)
// Inject to renderer instance
if (renderer) {
renderer.webpackDevMiddleware = this.webpackDevMiddleware
renderer.webpackHotMiddleware = this.webpackHotMiddleware
if (server) {
server.webpackDevMiddleware = this.webpackDevMiddleware
server.webpackHotMiddleware = this.webpackHotMiddleware
}
}

View File

@ -257,7 +257,7 @@ export default class WebpackBaseConfig {
const hasErrors = Object.values(states).some(state => state.stats.hasErrors())
if (!hasErrors) {
this.nuxt.showReady(false)
this.nuxt.server.showReady(false)
}
}
}

View File

@ -25,10 +25,9 @@ export default function rollupConfig({
return defaultsDeep({}, options, {
input: path.resolve(rootDir, input),
output: {
format: 'cjs',
sourcemap: false,
file: `${pkg.name.replace('-edge', '')}.js`,
dir: path.resolve(rootDir, 'dist')
dir: path.resolve(rootDir, 'dist'),
format: 'cjs'
},
preferConst: true,
external: [

View File

@ -13,7 +13,7 @@ describe('basic browser', () => {
const config = await loadFixture('basic')
nuxt = new Nuxt(config)
port = await getPort()
await nuxt.listen(port, 'localhost')
await nuxt.server.listen(port, 'localhost')
await browser.start({
// slowMo: 50,

View File

@ -12,7 +12,7 @@ const startServer = async (type = 'basic') => {
const config = await loadFixture(type)
nuxt = new Nuxt(config)
port = await getPort()
await nuxt.listen(port, 'localhost')
await nuxt.server.listen(port, 'localhost')
return nuxt
}

View File

@ -14,7 +14,7 @@ describe('children patch (browser)', () => {
const options = await loadFixture('children')
nuxt = new Nuxt(options)
port = await getPort()
await nuxt.listen(port, 'localhost')
await nuxt.server.listen(port, 'localhost')
})
test('Start browser', async () => {

View File

@ -8,7 +8,7 @@ const nuxtBin = resolve(__dirname, '../../../packages/cli/bin/nuxt.js')
describe('cli build', () => {
test('nuxt build', async () => {
const { stdout } = await execify(`node ${nuxtBin} build ${rootDir} -c cli.build.config.js`)
const { stdout } = await execify(`node -r esm ${nuxtBin} build ${rootDir} -c cli.build.config.js`)
expect(stdout.includes('Compiled successfully')).toBe(true)
}, 80000)

View File

@ -8,12 +8,12 @@ describe('basic ssr', () => {
const options = await loadFixture('async-config')
nuxt = new Nuxt(options)
port = await getPort()
await nuxt.listen(port, '0.0.0.0')
await nuxt.server.listen(port, '0.0.0.0')
})
test('/', async () => {
expect(nuxt.options.head.title).toBe('Async Config!')
const { html } = await nuxt.renderRoute('/')
const { html } = await nuxt.server.renderRoute('/')
expect(html.includes('<h1>I am ALIVE!</h1>')).toBe(true)
})
})

View File

@ -1,7 +1,7 @@
import { resolve } from 'path'
import consola from 'consola'
import { Nuxt, Options, version } from '../utils'
import { Nuxt, getNuxtConfig, version } from '../utils'
describe('basic config defaults', () => {
test('Nuxt.version is same as package', () => {
@ -9,13 +9,13 @@ describe('basic config defaults', () => {
})
test('modulesDir uses /node_modules as default if not set', () => {
const options = Options.from({})
const options = getNuxtConfig({})
const currentNodeModulesDir = resolve(__dirname, '..', '..', 'node_modules')
expect(options.modulesDir.includes(currentNodeModulesDir)).toBe(true)
})
test('vendor has been deprecated', () => {
const options = Options.from({
const options = getNuxtConfig({
build: { vendor: 'vue' }
})
expect(options.build.vendor).toBeUndefined()
@ -23,18 +23,18 @@ describe('basic config defaults', () => {
})
test('globalName uses nuxt as default if not set', () => {
const options = Options.from({})
const options = getNuxtConfig({})
expect(options.globalName).toEqual('nuxt')
})
test('globalName uses nuxt as default if set to something other than only letters', () => {
let options = Options.from({ globalName: '12foo4' })
let options = getNuxtConfig({ globalName: '12foo4' })
expect(options.globalName).toEqual('nuxt')
options = Options.from({ globalName: 'foo bar' })
options = getNuxtConfig({ globalName: 'foo bar' })
expect(options.globalName).toEqual('nuxt')
options = Options.from({ globalName: 'foo?' })
options = getNuxtConfig({ globalName: 'foo?' })
expect(options.globalName).toEqual('nuxt')
})
})

View File

@ -48,7 +48,7 @@ describe('basic dev', () => {
builder = new Builder(nuxt, BundleBuilder)
await builder.build()
port = await getPort()
await nuxt.listen(port, 'localhost')
await nuxt.server.listen(port, 'localhost')
})
test('Check build:done hook called', () => {
@ -83,7 +83,7 @@ describe('basic dev', () => {
})
test('/stateless', async () => {
const window = await nuxt.renderAndGetWindow(url('/stateless'))
const window = await nuxt.server.renderAndGetWindow(url('/stateless'))
const html = window.document.body.innerHTML
expect(html.includes('<h1>My component!</h1>')).toBe(true)
})
@ -117,7 +117,7 @@ describe('basic dev', () => {
})
test('/error should return error stack trace (Youch)', async () => {
await expect(nuxt.renderAndGetWindow(url('/error'))).rejects.toMatchObject({
await expect(nuxt.server.renderAndGetWindow(url('/error'))).rejects.toMatchObject({
statusCode: 500
})
})
@ -126,7 +126,7 @@ describe('basic dev', () => {
const sourceMaps = nuxt.renderer.resources.serverBundle.maps
nuxt.renderer.resources.serverBundle.maps = {}
await expect(nuxt.renderAndGetWindow(url('/error'))).rejects.toMatchObject({
await expect(nuxt.server.renderAndGetWindow(url('/error'))).rejects.toMatchObject({
statusCode: 500
})

View File

@ -85,19 +85,19 @@ describe('basic generate', () => {
})
test('/stateless', async () => {
const window = await generator.nuxt.renderAndGetWindow(url('/stateless'))
const window = await generator.nuxt.server.renderAndGetWindow(url('/stateless'))
const html = window.document.body.innerHTML
expect(html.includes('<h1>My component!</h1>')).toBe(true)
})
test('/store-module', async () => {
const window = await generator.nuxt.renderAndGetWindow(url('/store-module'))
const window = await generator.nuxt.server.renderAndGetWindow(url('/store-module'))
const html = window.document.body.innerHTML
expect(html.includes('<h1>mutated</h1>')).toBe(true)
})
test('/css', async () => {
const window = await generator.nuxt.renderAndGetWindow(url('/css'))
const window = await generator.nuxt.server.renderAndGetWindow(url('/css'))
const headHtml = window.document.head.innerHTML
expect(headHtml.includes('.red{color:red')).toBe(true)
@ -110,13 +110,13 @@ describe('basic generate', () => {
})
test('/stateful', async () => {
const window = await generator.nuxt.renderAndGetWindow(url('/stateful'))
const window = await generator.nuxt.server.renderAndGetWindow(url('/stateful'))
const html = window.document.body.innerHTML
expect(html.includes('<div><p>The answer is 42</p></div>')).toBe(true)
})
test('/head', async () => {
const window = await generator.nuxt.renderAndGetWindow(url('/head'))
const window = await generator.nuxt.server.renderAndGetWindow(url('/head'))
const html = window.document.body.innerHTML
const metas = window.document.getElementsByTagName('meta')
expect(window.document.title).toBe('My title - Nuxt.js')
@ -125,7 +125,7 @@ describe('basic generate', () => {
})
test('/async-data', async () => {
const window = await generator.nuxt.renderAndGetWindow(url('/async-data'))
const window = await generator.nuxt.server.renderAndGetWindow(url('/async-data'))
const html = window.document.body.innerHTML
expect(html.includes('<p>Nuxt.js</p>')).toBe(true)
})
@ -165,13 +165,13 @@ describe('basic generate', () => {
})
test('/validate -> should display a 404', async () => {
const window = await generator.nuxt.renderAndGetWindow(url('/validate'))
const window = await generator.nuxt.server.renderAndGetWindow(url('/validate'))
const html = window.document.body.innerHTML
expect(html.includes('This page could not be found')).toBe(true)
})
test('/validate?valid=true', async () => {
const window = await generator.nuxt.renderAndGetWindow(url('/validate?valid=true'))
const window = await generator.nuxt.server.renderAndGetWindow(url('/validate?valid=true'))
const html = window.document.body.innerHTML
expect(html.includes('I am valid</h1>')).toBe(true)
})
@ -183,7 +183,7 @@ describe('basic generate', () => {
})
test('/redirect -> check redirected source', async () => {
const window = await generator.nuxt.renderAndGetWindow(url('/redirect'))
const window = await generator.nuxt.server.renderAndGetWindow(url('/redirect'))
const html = window.document.body.innerHTML
expect(html.includes('<h1>Index page</h1>')).toBe(true)
})

View File

@ -10,11 +10,11 @@ describe('with-config', () => {
const config = await loadFixture('basic')
nuxt = new Nuxt(config)
port = await getPort()
await nuxt.listen(port, 'localhost')
await nuxt.server.listen(port, 'localhost')
})
test('/', async () => {
const window = await nuxt.renderAndGetWindow(url('/'))
const window = await nuxt.server.renderAndGetWindow(url('/'))
expect(window.__test_plugin).toBe(true)
})

View File

@ -10,7 +10,7 @@ const startCspServer = async (csp, isProduction = true) => {
})
const nuxt = new Nuxt(options)
port = await getPort()
await nuxt.listen(port, '0.0.0.0')
await nuxt.server.listen(port, '0.0.0.0')
return nuxt
}

View File

@ -11,16 +11,16 @@ describe('basic ssr', () => {
const options = await loadFixture('basic')
nuxt = new Nuxt(options)
port = await getPort()
await nuxt.listen(port, '0.0.0.0')
await nuxt.server.listen(port, '0.0.0.0')
})
test('/stateless', async () => {
const { html } = await nuxt.renderRoute('/stateless')
const { html } = await nuxt.server.renderRoute('/stateless')
expect(html.includes('<h1>My component!</h1>')).toBe(true)
})
test('/store-module', async () => {
const { html } = await nuxt.renderRoute('/store-module')
const { html } = await nuxt.server.renderRoute('/store-module')
expect(html.includes('<h1>mutated</h1>')).toBe(true)
})
@ -28,7 +28,7 @@ describe('basic ssr', () => {
** Example of testing via dom checking
*/
test('/css', async () => {
const window = await nuxt.renderAndGetWindow(url('/css'))
const window = await nuxt.server.renderAndGetWindow(url('/css'))
const headHtml = window.document.head.innerHTML
expect(headHtml.includes('color:red')).toBe(true)
@ -41,7 +41,7 @@ describe('basic ssr', () => {
})
test('/postcss', async () => {
const window = await nuxt.renderAndGetWindow(url('/css'))
const window = await nuxt.server.renderAndGetWindow(url('/css'))
const headHtml = window.document.head.innerHTML
expect(headHtml.includes('background-color:#00f')).toBe(true)
@ -51,18 +51,18 @@ describe('basic ssr', () => {
})
test('/stateful', async () => {
const { html } = await nuxt.renderRoute('/stateful')
const { html } = await nuxt.server.renderRoute('/stateful')
expect(html.includes('<div><p>The answer is 42</p></div>')).toBe(true)
})
test('/store', async () => {
const { html } = await nuxt.renderRoute('/store')
const { html } = await nuxt.server.renderRoute('/store')
expect(html.includes('<h1>Vuex Nested Modules</h1>')).toBe(true)
expect(html.includes('<p>1</p>')).toBe(true)
})
test('/head', async () => {
const window = await nuxt.renderAndGetWindow(url('/head'))
const window = await nuxt.server.renderAndGetWindow(url('/head'))
expect(window.document.title).toBe('My title - Nuxt.js')
const html = window.document.body.innerHTML
@ -77,64 +77,64 @@ describe('basic ssr', () => {
})
test('/async-data', async () => {
const { html } = await nuxt.renderRoute('/async-data')
const { html } = await nuxt.server.renderRoute('/async-data')
expect(html.includes('<p>Nuxt.js</p>')).toBe(true)
})
test('/await-async-data', async () => {
const { html } = await nuxt.renderRoute('/await-async-data')
const { html } = await nuxt.server.renderRoute('/await-async-data')
expect(html.includes('<p>Await Nuxt.js</p>')).toBe(true)
})
test('/callback-async-data', async () => {
const { html } = await nuxt.renderRoute('/callback-async-data')
const { html } = await nuxt.server.renderRoute('/callback-async-data')
expect(html.includes('<p>Callback Nuxt.js</p>')).toBe(true)
})
test('/users/1', async () => {
const { html } = await nuxt.renderRoute('/users/1')
const { html } = await nuxt.server.renderRoute('/users/1')
expect(html.includes('<h1>User: 1</h1>')).toBe(true)
})
test('/validate should display a 404', async () => {
const { html } = await nuxt.renderRoute('/validate')
const { html } = await nuxt.server.renderRoute('/validate')
expect(html.includes('This page could not be found')).toBe(true)
})
test('/validate-async should display a 404', async () => {
const { html } = await nuxt.renderRoute('/validate-async')
const { html } = await nuxt.server.renderRoute('/validate-async')
expect(html.includes('This page could not be found')).toBe(true)
})
test('/validate?valid=true', async () => {
const { html } = await nuxt.renderRoute('/validate?valid=true')
const { html } = await nuxt.server.renderRoute('/validate?valid=true')
expect(html.includes('<h1>I am valid</h1>')).toBe(true)
})
test('/validate-async?valid=true', async () => {
const { html } = await nuxt.renderRoute('/validate-async?valid=true')
const { html } = await nuxt.server.renderRoute('/validate-async?valid=true')
expect(html.includes('<h1>I am valid</h1>')).toBe(true)
})
test('/validate?error=403', async () => {
const { html, error } = await nuxt.renderRoute('/validate?error=403')
const { html, error } = await nuxt.server.renderRoute('/validate?error=403')
expect(error).toMatchObject({ statusCode: 403, message: 'Custom Error' })
expect(html.includes('Custom Error')).toBe(true)
})
test('/validate-async?error=503', async () => {
const { html, error } = await nuxt.renderRoute('/validate-async?error=503')
const { html, error } = await nuxt.server.renderRoute('/validate-async?error=503')
expect(error).toMatchObject({ statusCode: 503, message: 'Custom Error' })
expect(html.includes('Custom Error')).toBe(true)
})
test('/before-enter', async () => {
const { html } = await nuxt.renderRoute('/before-enter')
const { html } = await nuxt.server.renderRoute('/before-enter')
expect(html.includes('<h1>Index page</h1>')).toBe(true)
})
test('/redirect', async () => {
const { html, redirected } = await nuxt.renderRoute('/redirect')
const { html, redirected } = await nuxt.server.renderRoute('/redirect')
expect(html.includes('<div id="__nuxt"></div>')).toBe(true)
expect(redirected.path === '/').toBe(true)
expect(redirected.status === 302).toBe(true)
@ -142,14 +142,14 @@ describe('basic ssr', () => {
test('/redirect -> check redirected source', async () => {
// there are no transition properties in jsdom, ignore the error log
const window = await nuxt.renderAndGetWindow(url('/redirect'))
const window = await nuxt.server.renderAndGetWindow(url('/redirect'))
const html = window.document.body.innerHTML
expect(html.includes('<h1>Index page</h1>')).toBe(true)
})
test('/redirect -> external link', async () => {
let _headers, _status
const { html } = await nuxt.renderRoute('/redirect-external', {
const { html } = await nuxt.server.renderRoute('/redirect-external', {
res: {
writeHead(status, headers) {
_status = status
@ -164,13 +164,13 @@ describe('basic ssr', () => {
})
test('/special-state -> check window.__NUXT__.test = true', async () => {
const window = await nuxt.renderAndGetWindow(url('/special-state'))
const window = await nuxt.server.renderAndGetWindow(url('/special-state'))
expect(window.document.title).toBe('Nuxt.js')
expect(window.__NUXT__.test).toBe(true)
})
test('/error', async () => {
await expect(nuxt.renderRoute('/error', { req: {}, res: {} }))
await expect(nuxt.server.renderRoute('/error', { req: {}, res: {} }))
.rejects.toThrow('Error mouahahah')
})
@ -198,7 +198,7 @@ describe('basic ssr', () => {
})
test('/error2', async () => {
const { html, error } = await nuxt.renderRoute('/error2')
const { html, error } = await nuxt.server.renderRoute('/error2')
expect(html.includes('Custom error')).toBe(true)
expect(error.message.includes('Custom error')).toBe(true)
expect(error.statusCode === undefined).toBe(true)
@ -220,21 +220,21 @@ describe('basic ssr', () => {
})
test('/redirect-name', async () => {
const { html, redirected } = await nuxt.renderRoute('/redirect-name')
const { html, redirected } = await nuxt.server.renderRoute('/redirect-name')
expect(html.includes('<div id="__nuxt"></div>')).toBe(true)
expect(redirected.path === '/stateless').toBe(true)
expect(redirected.status === 302).toBe(true)
})
test('/no-ssr', async () => {
const { html } = await nuxt.renderRoute('/no-ssr')
const { html } = await nuxt.server.renderRoute('/no-ssr')
expect(html.includes(
'<p class="no-ssr-placeholder">Loading...</p>'
)).toBe(true)
})
test('/no-ssr (client-side)', async () => {
const window = await nuxt.renderAndGetWindow(url('/no-ssr'))
const window = await nuxt.server.renderAndGetWindow(url('/no-ssr'))
const html = window.document.body.innerHTML
expect(html.includes('Displayed only on client-side</h1>')).toBe(true)
})
@ -259,7 +259,7 @@ describe('basic ssr', () => {
})
test('/meta', async () => {
const { html } = await nuxt.renderRoute('/meta')
const { html } = await nuxt.server.renderRoute('/meta')
expect(/<pre>.*&quot;works&quot;: true.*<\/pre>/s.test(html)).toBe(true)
})
@ -269,28 +269,28 @@ describe('basic ssr', () => {
})
test('/fn-midd?please=true', async () => {
const { html } = await nuxt.renderRoute('/fn-midd?please=true')
const { html } = await nuxt.server.renderRoute('/fn-midd?please=true')
expect(html.includes('<h1>Date:')).toBe(true)
})
test('/router-guard', async () => {
const { html } = await nuxt.renderRoute('/router-guard')
const { html } = await nuxt.server.renderRoute('/router-guard')
expect(html.includes('<p>Nuxt.js</p>')).toBe(true)
expect(html.includes('Router Guard')).toBe(false)
})
test('/jsx', async () => {
const { html } = await nuxt.renderRoute('/jsx')
const { html } = await nuxt.server.renderRoute('/jsx')
expect(html.includes('<h1>JSX Page</h1>')).toBe(true)
})
test('/jsx-link', async () => {
const { html } = await nuxt.renderRoute('/jsx-link')
const { html } = await nuxt.server.renderRoute('/jsx-link')
expect(html.includes('<h1>JSX Link Page</h1>')).toBe(true)
})
test('/js-link', async () => {
const { html } = await nuxt.renderRoute('/js-link')
const { html } = await nuxt.server.renderRoute('/js-link')
expect(html.includes('<h1>vue file is first-class</h1>')).toBe(true)
})

View File

@ -10,39 +10,39 @@ describe('children', () => {
const options = await loadFixture('children')
nuxt = new Nuxt(options)
port = await getPort()
await nuxt.listen(port, 'localhost')
await nuxt.server.listen(port, 'localhost')
})
test('/parent', async () => {
const { html } = await nuxt.renderRoute('/parent')
const { html } = await nuxt.server.renderRoute('/parent')
expect(html.includes('<h1>I am the parent</h1>')).toBe(true)
})
test('/parent/child', async () => {
const { html } = await nuxt.renderRoute('/parent/child')
const { html } = await nuxt.server.renderRoute('/parent/child')
expect(html.includes('<h1>I am the parent</h1>')).toBe(true)
expect(html.includes('<h2>I am the child</h2>')).toBe(true)
})
test('/parent should call _id.vue', async () => {
const { html } = await nuxt.renderRoute('/parent')
const { html } = await nuxt.server.renderRoute('/parent')
expect(html.includes('<h1>I am the parent</h1>')).toBe(true)
expect(html.includes('<h2>Id=</h2>')).toBe(true)
})
test('/parent/1', async () => {
const { html } = await nuxt.renderRoute('/parent/1')
const { html } = await nuxt.server.renderRoute('/parent/1')
expect(html.includes('<h1>I am the parent</h1>')).toBe(true)
expect(html.includes('<h2>Id=1</h2>')).toBe(true)
})
test('/parent/validate-child should display 404', async () => {
const { html } = await nuxt.renderRoute('/parent/validate-child')
const { html } = await nuxt.server.renderRoute('/parent/validate-child')
expect(html.includes('This page could not be found')).toBe(true)
})
test('/parent/validate-child?key=12345', async () => {
const { html } = await nuxt.renderRoute('/parent/validate-child?key=12345')
const { html } = await nuxt.server.renderRoute('/parent/validate-child?key=12345')
expect(html.includes('<h1>I am the parent</h1>')).toBe(true)
expect(html.includes('<h2>Child valid</h2>')).toBe(true)
})

Some files were not shown because too many files have changed in this diff Show More