feat: improve SSR bundle (#4439)

- Better insights and inspection for server bundle
- Remove all vue related dependencies from vue-renderer package as much as possible to reduce install size of nuxt-start
- Support for single file distributions (serverless)
- Remove server-bundle.json and use the standard .js files for dist/server
- Mitigate CALL_AND_RETRY_LAST Allocation failed errors. Most of the cases happen on JSON.parse() the part when loading bundle. (#4225, #3465, #1728, #1601, #1481)
- Reduce server dist size by removing escape characters caused by JSON serialize
- Faster dev reloads and production start by removing extra JSON.serialize/JSON.parse time
- Less memory usage
- General performance improvements and refactors
This commit is contained in:
Pooya Parsa 2018-12-01 13:43:28 +03:30 committed by GitHub
parent 06ddfbb77b
commit 0f104aa588
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 329 additions and 323 deletions

View File

@ -72,7 +72,9 @@ export default class NuxtCommand {
async getNuxt(options) {
const { Nuxt } = await imports.core()
return new Nuxt(options)
const nuxt = new Nuxt(options)
await nuxt.ready()
return nuxt
}
async getBuilder(nuxt) {

View File

@ -1,6 +1,3 @@
import fs from 'fs'
import path from 'path'
import consola from 'consola'
import { common, server } from '../options'
import { showBanner } from '../utils'
@ -17,32 +14,10 @@ export default {
// Create production build when calling `nuxt build`
const nuxt = await cmd.getNuxt(
await cmd.getNuxtConfig(argv, { dev: false })
await cmd.getNuxtConfig(argv, { dev: false, _start: true })
)
// Check if project is built for production
const distDir = path.resolve(
nuxt.options.rootDir,
nuxt.options.buildDir || '.nuxt',
'dist',
'server'
)
if (!fs.existsSync(distDir)) {
consola.fatal(
'No build files found, please run `nuxt build` before launching `nuxt start`'
)
}
// Check if SSR Bundle is required
if (nuxt.options.render.ssr === true) {
const ssrBundlePath = path.resolve(distDir, 'server-bundle.json')
if (!fs.existsSync(ssrBundlePath)) {
consola.fatal(
'No SSR build found.\nPlease start with `nuxt start --spa` or build using `nuxt build --universal`'
)
}
}
// Listen and show ready banner
return nuxt.server.listen().then(() => {
showBanner(nuxt)
})

View File

@ -1,4 +1,4 @@
import fs from 'fs'
import fs from 'fs-extra'
import { consola, mockGetNuxtStart, mockGetNuxtConfig, NuxtCommand } from '../utils'
describe('start', () => {
@ -22,41 +22,14 @@ describe('start', () => {
test('no error if dist dir exists', async () => {
mockGetNuxtStart()
mockGetNuxtConfig()
jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => true)
await NuxtCommand.from(start).run()
expect(consola.fatal).not.toHaveBeenCalled()
})
test('fatal error if dist dir doesnt exist', async () => {
mockGetNuxtStart()
jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => false)
await NuxtCommand.from(start).run()
expect(consola.fatal).toHaveBeenCalledWith('No build files found, please run `nuxt build` before launching `nuxt start`')
})
test('no error on ssr and server bundle exists', async () => {
mockGetNuxtStart(true)
mockGetNuxtConfig()
jest.spyOn(fs, 'existsSync').mockImplementation(() => true)
await NuxtCommand.from(start).run()
expect(consola.fatal).not.toHaveBeenCalled()
})
test.skip('fatal error on ssr and server bundle doesnt exist', async () => {
mockGetNuxtStart(true)
let i = 0
jest.spyOn(fs, 'existsSync').mockImplementation(() => {
return ++i === 1
})
await NuxtCommand.from(start).run()
expect(consola.fatal).toHaveBeenCalledWith('No SSR build found.\nPlease start with `nuxt start --spa` or build using `nuxt build --universal`')
})
})

View File

@ -91,6 +91,7 @@ export const mockNuxt = (implementation) => {
options: {},
clearHook: jest.fn(),
close: jest.fn(),
ready: jest.fn(),
server: {
listeners: [],
listen: jest.fn().mockImplementationOnce(() => Promise.resolve())

View File

@ -64,8 +64,7 @@ export default ({ resources, options }) => function errorMiddleware(err, req, re
readSourceFactory({
srcDir: options.srcDir,
rootDir: options.rootDir,
buildDir: options.buildDir,
resources
buildDir: options.buildDir
}),
options.router.base,
true
@ -79,7 +78,7 @@ export default ({ resources, options }) => function errorMiddleware(err, req, re
}
}
const readSourceFactory = ({ srcDir, rootDir, buildDir, resources }) => async function readSource(frame) {
const readSourceFactory = ({ srcDir, rootDir, buildDir }) => async function readSource(frame) {
// Remove webpack:/// & query string from the end
const sanitizeName = name =>
name ? name.replace('webpack:///', '').split('?')[0] : null
@ -113,11 +112,4 @@ const readSourceFactory = ({ srcDir, rootDir, buildDir, resources }) => async fu
return
}
}
// Fallback: use server bundle
// TODO: restore to if after https://github.com/istanbuljs/nyc/issues/595 fixed
/* istanbul ignore next */
if (!frame.contents) {
frame.contents = resources.serverBundle.files[frame.fileName]
}
}

View File

@ -10,6 +10,14 @@
],
"main": "dist/vue-app.js",
"typings": "types/index.d.ts",
"dependencies": {
"vue": "^2.5.17",
"vue-meta": "^1.5.6",
"vue-no-ssr": "^1.1.0",
"vue-router": "^3.0.2",
"vue-template-compiler": "^2.5.17",
"vuex": "^3.0.1"
},
"publishConfig": {
"access": "public"
}

View File

@ -15,11 +15,7 @@
"lru-cache": "^5.1.1",
"vue": "^2.5.17",
"vue-meta": "^1.5.6",
"vue-no-ssr": "^1.1.0",
"vue-router": "^3.0.2",
"vue-server-renderer": "^2.5.17",
"vue-template-compiler": "^2.5.17",
"vuex": "^3.0.1"
"vue-server-renderer": "^2.5.17"
},
"publishConfig": {
"access": "public"

View File

@ -1,6 +1,6 @@
import path from 'path'
import crypto from 'crypto'
import fs from 'fs-extra'
import fs from 'fs'
import consola from 'consola'
import devalue from '@nuxtjs/devalue'
import invert from 'lodash/invert'
@ -16,19 +16,19 @@ export default class VueRenderer {
// Will be set by createRenderer
this.renderer = {
ssr: null,
modern: null,
spa: null
ssr: undefined,
modern: undefined,
spa: undefined
}
// Renderer runtime resources
Object.assign(this.context.resources, {
clientManifest: null,
modernManifest: null,
serverBundle: null,
ssrTemplate: null,
spaTemplate: null,
errorTemplate: this.constructor.parseTemplate('Nuxt.js Internal Server Error')
clientManifest: undefined,
modernManifest: undefined,
serverManifest: undefined,
ssrTemplate: undefined,
spaTemplate: undefined,
errorTemplate: this.parseTemplate('Nuxt.js Internal Server Error')
})
}
@ -98,57 +98,78 @@ export default class VueRenderer {
}
async ready() {
// Production: Load SSR resources from fs
if (!this.context.options.dev) {
await this.loadResources()
// Production: Load SSR resources from fs
await this.loadResources(fs)
// Verify
if (!this.isReady && this.context.options._start) {
throw new Error(
'No build files found. Use either `nuxt build` or `builder.build()` or start nuxt in development mode.'
)
}
} else {
// Development: Listen on build:resources hook
this.context.nuxt.hook('build:resources', mfs => this.loadResources(mfs, true))
}
}
async loadResources(_fs = fs) {
loadResources(_fs, isMFS = false) {
const distPath = path.resolve(this.context.options.buildDir, 'dist', 'server')
const updated = []
const resourceMap = this.resourceMap
this.constructor.resourceMap.forEach(({ key, fileName, transform }) => {
const rawKey = '$$' + key
const _path = path.join(distPath, fileName)
const readResource = (fileName, encoding) => {
try {
const fullPath = path.resolve(distPath, fileName)
if (!_fs.existsSync(fullPath)) {
return
}
const contents = _fs.readFileSync(fullPath, encoding)
if (isMFS) {
// Cleanup MFS as soon as possible to save memory
_fs.unlinkSync(fullPath)
}
return contents
} catch (err) {
consola.error('Unable to load resource:', fileName, err)
}
}
// Fail when no build found and using programmatic usage
if (!_fs.existsSync(_path)) {
// TODO: Enable baack when renderer initialzation was disabled for build only scripts
// Currently this breaks normal nuxt build for first time
// if (!this.context.options.dev) {
// const invalidSSR = !this.noSSR && key === 'serverBundle'
// const invalidSPA = this.noSSR && key === 'spaTemplate'
// if (invalidSPA || invalidSSR) {
// consola.fatal(`Could not load Nuxt renderer, make sure to build for production: builder.build() with dev option set to false.`)
// }
// }
return // Resource not exists
for (const resourceName in resourceMap) {
const { fileName, transform, encoding } = resourceMap[resourceName]
// Load resource
let resource = readResource(fileName, encoding)
// Skip unavailable resources
if (!resource) {
consola.debug('Resource not available:', resourceName)
continue
}
const rawData = _fs.readFileSync(_path, 'utf8')
if (!rawData || rawData === this.context.resources[rawKey]) {
return // No changes
// Apply transforms
if (typeof transform === 'function') {
resource = transform(resource, {
readResource,
oldValue: this.context.resources[resourceName]
})
}
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)
})
// Update resource
this.context.resources[resourceName] = resource
updated.push(resourceName)
}
// 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(
this.context.resources.errorTemplate = this.parseTemplate(
fs.readFileSync(errorTemplatePath, 'utf8')
)
}
// Load loading template
// Reload loading template
const loadingHTMLPath = path.resolve(this.context.options.buildDir, 'loading.html')
if (fs.existsSync(loadingHTMLPath)) {
this.context.resources.loadingHTML = fs.readFileSync(loadingHTMLPath, 'utf8')
@ -158,57 +179,60 @@ export default class VueRenderer {
this.context.resources.loadingHTML = ''
}
// Call resourcesLoaded plugin
await this.context.nuxt.callHook('render:resourcesLoaded', this.context.resources)
// Call createRenderer if any resource changed
if (updated.length > 0) {
this.createRenderer()
}
// Call resourcesLoaded hook
consola.debug('Resources loaded:', updated.join(','))
return this.context.nuxt.callHook('render:resourcesLoaded', this.context.resources)
}
get noSSR() {
get noSSR() { /* Backward compatibility */
return this.context.options.render.ssr === false
}
get isReady() {
if (this.noSSR) {
return Boolean(this.context.resources.spaTemplate)
}
return Boolean(this.renderer.ssr && this.context.resources.ssrTemplate)
get SSR() {
return this.context.options.render.ssr === true
}
get isResourcesAvailable() {
// Required for both
/* istanbul ignore if */
if (!this.context.resources.clientManifest) {
get isReady() {
// SPA
if (!this.context.resources.spaTemplate || !this.renderer.spa) {
return false
}
// Required for SPA rendering
if (this.noSSR) {
return Boolean(this.context.resources.spaTemplate)
// SSR
if (this.SSR && (!this.context.resources.ssrTemplate || !this.renderer.ssr)) {
return false
}
// Required for bundle renderer
return Boolean(this.context.resources.ssrTemplate && this.context.resources.serverBundle)
return true
}
get isResourcesAvailable() { /* Backward compatibility */
return this.isReady
}
createRenderer() {
// Ensure resources are available
if (!this.isResourcesAvailable) {
// Resource clientManifest is always required
if (!this.context.resources.clientManifest) {
return
}
// Create Meta Renderer
this.renderer.spa = new SPAMetaRenderer(this)
// Create SPA renderer
if (this.context.resources.spaTemplate) {
this.renderer.spa = new SPAMetaRenderer(this)
}
// Skip following steps if noSSR mode
if (this.noSSR) {
// Skip the rest if SSR resources are not available
if (!this.context.resources.ssrTemplate || !this.context.resources.serverManifest) {
return
}
const hasModules = fs.existsSync(path.resolve(this.context.options.rootDir, 'node_modules'))
const rendererOptions = {
runInNewContext: false,
clientManifest: this.context.resources.clientManifest,
@ -219,14 +243,14 @@ export default class VueRenderer {
// Create bundle renderer for SSR
this.renderer.ssr = createBundleRenderer(
this.context.resources.serverBundle,
this.context.resources.serverManifest,
rendererOptions
)
if (this.context.resources.modernManifest &&
!['client', false].includes(this.context.options.modern)) {
this.renderer.modern = createBundleRenderer(
this.context.resources.serverBundle,
this.context.resources.serverManifest,
{
...rendererOptions,
clientManifest: this.context.resources.modernManifest
@ -248,6 +272,7 @@ export default class VueRenderer {
async renderRoute(url, context = {}) {
/* istanbul ignore if */
if (!this.isReady) {
consola.info('Waiting for server resources...')
await waitFor(1000)
return this.renderRoute(url, context)
}
@ -258,12 +283,12 @@ export default class VueRenderer {
// Add url and isSever to the context
context.url = url
// Basic response if SSR is disabled or spa data provided
// Basic response if SSR is disabled or SPA data provided
const { req, res } = context
const spa = context.spa || (res && res.spa)
const ENV = this.context.options.env
if (this.noSSR || spa) {
if (!this.SSR || spa) {
const {
HTML_ATTRS,
BODY_ATTRS,
@ -348,39 +373,57 @@ export default class VueRenderer {
}
}
static parseTemplate(templateStr) {
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: (src, { readResource, oldValue = { files: {}, maps: {} } }) => {
const serverManifest = JSON.parse(src)
const resolveAssets = (obj, oldObj, encoding) => {
Object.keys(obj).forEach((name) => {
obj[name] = readResource(obj[name], encoding)
// Try to reuse deleted MFS files if no new version exists
if (!obj[name]) {
obj[name] = oldObj[name]
}
})
return obj
}
const files = resolveAssets(serverManifest.files, oldValue.files)
const maps = resolveAssets(serverManifest.maps, oldValue.maps, 'utf-8')
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
})
}
static get resourceMap() {
return [
{
key: 'clientManifest',
fileName: 'vue-ssr-client-manifest.json',
transform: JSON.parse
},
{
key: 'modernManifest',
fileName: 'vue-ssr-modern-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

@ -1,4 +1,3 @@
import fs from 'fs'
import path from 'path'
import pify from 'pify'
import webpack from 'webpack'
@ -14,7 +13,8 @@ import {
wrapArray
} from '@nuxt/common'
import { ClientConfig, ModernConfig, ServerConfig, PerfLoader } from './config'
import { ClientConfig, ModernConfig, ServerConfig } from './config'
import PerfLoader from './utils/perf-loader'
const glob = pify(Glob)
@ -27,9 +27,11 @@ export class WebpackBundler {
this.devMiddleware = {}
this.hotMiddleware = {}
// Initialize shared FS and Cache
// Initialize shared MFS for dev
if (this.context.options.dev) {
this.mfs = new MFS()
this.mfs.exists = function (...args) { return Promise.resolve(this.existsSync(...args)) }
this.mfs.readFile = function (...args) { return Promise.resolve(this.readFileSync(...args)) }
}
}
@ -82,7 +84,7 @@ export class WebpackBundler {
'Please use https://github.com/nuxt-community/style-resources-module'
)
}
Object.keys(styleResources).forEach(async (ext) => {
for (const ext of Object.keys(styleResources)) {
await Promise.all(wrapArray(styleResources[ext]).map(async (p) => {
const styleResourceFiles = await glob(path.resolve(this.context.options.rootDir, p))
@ -90,7 +92,7 @@ export class WebpackBundler {
throw new Error(`Style Resource not found: ${p}`)
}
}))
})
}
// Configure compilers
this.compilers = compilersOptions.map((compilersOption) => {
@ -135,7 +137,7 @@ export class WebpackBundler {
})
// Reload renderer if available
nuxt.server.loadResources(this.mfs || fs)
await nuxt.callHook('build:resources', this.mfs)
// Resolve on next tick
process.nextTick(resolve)

View File

@ -6,14 +6,15 @@ import cloneDeep from 'lodash/cloneDeep'
import escapeRegExp from 'lodash/escapeRegExp'
import VueLoader from 'vue-loader'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import TerserWebpackPlugin from 'terser-webpack-plugin'
import WebpackBar from 'webpackbar'
import env from 'std-env'
import { isUrl, urlJoin } from '@nuxt/common'
import PerfLoader from './utils/perf-loader'
import StyleLoader from './utils/style-loader'
import WarnFixPlugin from './plugins/warnfix'
import PerfLoader from '../utils/perf-loader'
import StyleLoader from '../utils/style-loader'
import WarnFixPlugin from '../plugins/warnfix'
export default class WebpackBaseConfig {
constructor(builder, options) {
@ -96,7 +97,7 @@ export default class WebpackBaseConfig {
return fileName
}
devtool() {
get devtool() {
return false
}
@ -127,7 +128,41 @@ export default class WebpackBaseConfig {
}
optimization() {
return this.options.build.optimization
const optimization = cloneDeep(this.options.build.optimization)
if (optimization.minimize && optimization.minimizer === undefined) {
optimization.minimizer = this.minimizer()
}
return optimization
}
minimizer() {
const minimizer = []
// https://github.com/webpack-contrib/terser-webpack-plugin
if (this.options.build.terser) {
minimizer.push(
new TerserWebpackPlugin(Object.assign({
parallel: true,
cache: this.options.build.cache,
sourceMap: this.devtool && /source-?map/.test(this.devtool),
extractComments: {
filename: 'LICENSES'
},
terserOptions: {
compress: {
ecma: this.isModern ? 6 : undefined
},
output: {
comments: /^\**!|@preserve|@license|@cc_on/
}
}
}, this.options.build.terser))
)
}
return minimizer
}
alias() {
@ -337,10 +372,11 @@ export default class WebpackBaseConfig {
config() {
// Prioritize nested node_modules in webpack search path (#2558)
const webpackModulesDir = ['node_modules'].concat(this.options.modulesDir)
const config = {
name: this.name,
mode: this.buildMode,
devtool: this.devtool(),
devtool: this.devtool,
optimization: this.optimization(),
output: this.output(),
performance: {

View File

@ -2,12 +2,11 @@ import path from 'path'
import webpack from 'webpack'
import HTMLPlugin from 'html-webpack-plugin'
import BundleAnalyzer from 'webpack-bundle-analyzer'
import TerserWebpackPlugin from 'terser-webpack-plugin'
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'
import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin'
import ModernModePlugin from './plugins/vue/modern'
import VueSSRClientPlugin from './plugins/vue/client'
import ModernModePlugin from '../plugins/vue/modern'
import VueSSRClientPlugin from '../plugins/vue/client'
import WebpackBaseConfig from './base'
export default class WebpackClientConfig extends WebpackBaseConfig {
@ -54,6 +53,21 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
return optimization
}
minimizer() {
const minimizer = super.minimizer()
// https://github.com/NMFR/optimize-css-assets-webpack-plugin
// https://github.com/webpack-contrib/mini-css-extract-plugin#minimizing-for-production
// TODO: Remove OptimizeCSSAssetsPlugin when upgrading to webpack 5
if (this.options.build.optimizeCSS) {
minimizer.push(
new OptimizeCSSAssetsPlugin(Object.assign({}, this.options.build.optimizeCSS))
)
}
return minimizer
}
plugins() {
const plugins = super.plugins()
@ -78,7 +92,7 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
chunksSortMode: 'dependency'
}),
new VueSSRClientPlugin({
filename: `../server/vue-ssr-${this.name}-manifest.json`
filename: `../server/${this.name}.manifest.json`
}),
new webpack.DefinePlugin(this.env())
)
@ -113,48 +127,6 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
return plugins
}
extendConfig() {
const config = super.extendConfig(...arguments)
// Add minimizer plugins
if (config.optimization.minimize && config.optimization.minimizer === undefined) {
config.optimization.minimizer = []
// https://github.com/webpack-contrib/terser-webpack-plugin
if (this.options.build.terser) {
config.optimization.minimizer.push(
new TerserWebpackPlugin(Object.assign({
parallel: true,
cache: this.options.build.cache,
sourceMap: config.devtool && /source-?map/.test(config.devtool),
extractComments: {
filename: 'LICENSES'
},
terserOptions: {
compress: {
ecma: this.isModern ? 6 : undefined
},
output: {
comments: /^\**!|@preserve|@license|@cc_on/
}
}
}, this.options.build.terser))
)
}
// https://github.com/NMFR/optimize-css-assets-webpack-plugin
// https://github.com/webpack-contrib/mini-css-extract-plugin#minimizing-for-production
// TODO: Remove OptimizeCSSAssetsPlugin when upgrading to webpack 5
if (this.options.build.optimizeCSS) {
config.optimization.minimizer.push(
new OptimizeCSSAssetsPlugin(Object.assign({}, this.options.build.optimizeCSS))
)
}
}
return config
}
config() {
const config = super.config()

View File

@ -1,4 +1,3 @@
export { default as ClientConfig } from './client'
export { default as ModernConfig } from './modern'
export { default as ServerConfig } from './server'
export { default as PerfLoader } from './utils/perf-loader'

View File

@ -4,8 +4,9 @@ import webpack from 'webpack'
import escapeRegExp from 'lodash/escapeRegExp'
import nodeExternals from 'webpack-node-externals'
import VueSSRServerPlugin from '../plugins/vue/server'
import WebpackBaseConfig from './base'
import VueSSRServerPlugin from './plugins/vue/server'
export default class WebpackServerConfig extends WebpackBaseConfig {
constructor(builder) {
@ -29,8 +30,8 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
return whitelist
}
devtool() {
return 'cheap-module-inline-source-map'
get devtool() {
return 'cheap-module-source-map'
}
env() {
@ -45,7 +46,7 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
optimization() {
return {
splitChunks: false,
minimizer: []
minimizer: this.minimizer()
}
}
@ -53,7 +54,7 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
const plugins = super.plugins()
plugins.push(
new VueSSRServerPlugin({
filename: 'server-bundle.json'
filename: `${this.name}.manifest.json`
}),
new webpack.DefinePlugin(this.env())
)
@ -70,11 +71,12 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
app: [path.resolve(this.options.buildDir, 'server.js')]
},
output: Object.assign({}, config.output, {
filename: 'server-bundle.js',
filename: 'server.js',
libraryTarget: 'commonjs2'
}),
performance: {
hints: false,
maxEntrypointSize: Infinity,
maxAssetSize: Infinity
},
externals: []

View File

@ -1,3 +1,8 @@
/**
* This file is based on Vue.js (MIT) webpack plugins
* https://github.com/vuejs/vue/blob/dev/src/server/webpack-plugin/client.js
*/
import hash from 'hash-sum'
import uniq from 'lodash/uniq'
@ -6,7 +11,7 @@ import { isJS, isCSS, onEmit } from './util'
export default class VueSSRClientPlugin {
constructor(options = {}) {
this.options = Object.assign({
filename: 'vue-ssr-client-manifest.json'
filename: null
}, options)
}
@ -45,7 +50,7 @@ export default class VueSSRClientPlugin {
const assetModules = stats.modules.filter(m => m.assets.length)
const fileToIndex = file => manifest.all.indexOf(file)
stats.modules.forEach((m) => {
// ignore modules duplicated in multiple chunks
// Ignore modules duplicated in multiple chunks
if (m.chunks.length === 1) {
const cid = m.chunks[0]
const chunk = stats.chunks.find(c => c.id === cid)
@ -54,7 +59,8 @@ export default class VueSSRClientPlugin {
}
const id = m.identifier.replace(/\s\w+$/, '') // remove appended hash
const files = manifest.modules[hash(id)] = chunk.files.map(fileToIndex)
// find all asset modules associated with the same chunk
// Find all asset modules associated with the same chunk
assetModules.forEach((m) => {
if (m.chunks.some(id => id === cid)) {
files.push.apply(files, m.assets.map(fileToIndex))
@ -63,16 +69,11 @@ export default class VueSSRClientPlugin {
}
})
// const debug = (file, obj) => {
// require('fs').writeFileSync(__dirname + '/' + file, JSON.stringify(obj, null, 2))
// }
// debug('stats.json', stats)
// debug('client-manifest.json', manifest)
const src = JSON.stringify(manifest, null, 2)
const json = JSON.stringify(manifest, null, 2)
compilation.assets[this.options.filename] = {
source: () => json,
size: () => json.length
source: () => src,
size: () => src.length
}
cb()
})

View File

@ -1,16 +1,14 @@
/*
** This plugin is inspired by @vue/cli-service ModernModePlugin
** https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js
* This file is based on @vue/cli-service (MIT) ModernModePlugin
* https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js
*/
import EventEmitter from 'events'
// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
const safariFix = `!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()}}();`
import EventEmitter from 'events'
const assetsMap = {}
const watcher = new EventEmitter()
class ModernModePlugin {
export default class ModernModePlugin {
constructor({ targetDir, isModernBuild }) {
this.targetDir = targetDir
this.isModernBuild = isModernBuild
@ -83,7 +81,7 @@ class ModernModePlugin {
data.body.push({
tagName: 'script',
closeTag: true,
innerHTML: safariFix
innerHTML: ModernModePlugin.safariFix
})
// inject links for legacy assets as <script nomodule>
@ -107,6 +105,5 @@ class ModernModePlugin {
}
}
ModernModePlugin.safariFix = safariFix
export default ModernModePlugin
// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
ModernModePlugin.safariFix = `!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

@ -3,7 +3,7 @@ import { validate, isJS, onEmit } from './util'
export default class VueSSRServerPlugin {
constructor(options = {}) {
this.options = Object.assign({
filename: 'vue-ssr-server-bundle.json'
filename: null
}, options)
}
@ -44,20 +44,17 @@ export default class VueSSRServerPlugin {
stats.assets.forEach((asset) => {
if (isJS(asset.name)) {
bundle.files[asset.name] = compilation.assets[asset.name].source()
bundle.files[asset.name] = asset.name
} else if (asset.name.match(/\.js\.map$/)) {
bundle.maps[asset.name.replace(/\.map$/, '')] = JSON.parse(compilation.assets[asset.name].source())
bundle.maps[asset.name.replace(/\.map$/, '')] = asset.name
}
// do not emit anything else for server
delete compilation.assets[asset.name]
})
const json = JSON.stringify(bundle, null, 2)
const filename = this.options.filename
const src = JSON.stringify(bundle, null, 2)
compilation.assets[filename] = {
source: () => json,
size: () => json.length
compilation.assets[this.options.filename] = {
source: () => src,
size: () => src.length
}
cb()

View File

@ -1,20 +1,21 @@
import chalk from 'chalk'
/**
* This file is based on Vue.js (MIT) webpack plugins
* https://github.com/vuejs/vue/blob/dev/src/server/webpack-plugin/util.js
*/
const prefix = `[vue-server-renderer-webpack-plugin]`
export const warn = msg => console.error(chalk.red(`${prefix} ${msg}\n`)) // eslint-disable-line no-console
export const tip = msg => console.log(chalk.yellow(`${prefix} ${msg}\n`)) // eslint-disable-line no-console
import consola from 'consola'
export const validate = (compiler) => {
if (compiler.options.target !== 'node') {
warn('webpack config `target` should be "node".')
consola.warn('webpack config `target` should be "node".')
}
if (compiler.options.output && compiler.options.output.libraryTarget !== 'commonjs2') {
warn('webpack config `output.libraryTarget` should be "commonjs2".')
consola.warn('webpack config `output.libraryTarget` should be "commonjs2".')
}
if (!compiler.options.externals) {
tip(
consola.info(
'It is recommended to externalize dependencies in the server build for ' +
'better build performance.'
)

View File

@ -1,5 +1,7 @@
export default {
styleResources: {
'stylus': './nothinghere'
build: {
styleResources: {
'stylus': './nothinghere'
}
}
}

View File

@ -143,17 +143,6 @@ describe('basic dev', () => {
})
})
test('/error no source-map (Youch)', async () => {
const sourceMaps = nuxt.renderer.resources.serverBundle.maps
nuxt.renderer.resources.serverBundle.maps = {}
await expect(nuxt.server.renderAndGetWindow(url('/error'))).rejects.toMatchObject({
statusCode: 500
})
nuxt.renderer.resources.serverBundle.maps = sourceMaps
})
test('/error should return json format error (Youch)', async () => {
const opts = {
headers: {

View File

@ -282,11 +282,6 @@ describe('basic ssr', () => {
.rejects.toMatchObject({ statusCode: 304 })
})
test('/_nuxt/server-bundle.json should return 404', async () => {
await expect(rp(url('/_nuxt/server-bundle.json')))
.rejects.toMatchObject({ statusCode: 404 })
})
test('/_nuxt/ should return 404', async () => {
await expect(rp(url('/_nuxt/')))
.rejects.toMatchObject({ statusCode: 404 })

View File

@ -19,22 +19,25 @@ describe('nuxt', () => {
expect(nuxt.initialized).toBe(true)
})
test('Fail to build when no pages/ directory but is in the parent', () => {
test('Fail to build when no pages/ directory but is in the parent', async () => {
const config = await loadFixture('empty')
const nuxt = new Nuxt({
dev: false,
...config,
rootDir: resolve(__dirname, '..', 'fixtures', 'empty', 'pages')
})
return new Builder(nuxt).build().catch((err) => {
const s = String(err)
expect(s).toContain('No `pages` directory found')
expect(s).toContain('Did you mean to run `nuxt` in the parent (`../`) directory?')
})
try {
await new Builder(nuxt).build()
} catch (err) {
expect(err.message).toContain('No `pages` directory found')
expect(err.message).toContain('Did you mean to run `nuxt` in the parent (`../`) directory?')
}
expect.hasAssertions()
})
test('Build with default page when no pages/ directory', async () => {
const nuxt = new Nuxt()
new Builder(nuxt).build()
const config = await loadFixture('missing-pages-dir')
const nuxt = new Nuxt(config)
const port = await getPort()
await nuxt.server.listen(port, 'localhost')
@ -45,27 +48,17 @@ describe('nuxt', () => {
await nuxt.close()
})
test('Fail to build when specified plugin isn\'t found', () => {
const nuxt = new Nuxt({
dev: false,
rootDir: resolve(__dirname, '..', 'fixtures', 'missing-plugin')
})
test('Fail to build when specified plugin isn\'t found', async () => {
const config = await loadFixture('missing-plugin')
const nuxt = new Nuxt(config)
return new Builder(nuxt).build().catch((err) => {
const s = String(err)
expect(s).toContain('Plugin not found')
})
await expect(new Builder(nuxt).build()).rejects.toThrow('Plugin not found')
})
test('Warn when styleResource isn\'t found', () => {
const nuxt = new Nuxt({
dev: false,
rootDir: resolve(__dirname, '..', 'fixtures', 'missing-style-resource')
})
test('Warn when styleResource isn\'t found', async () => {
const config = await loadFixture('missing-style-resource')
const nuxt = new Nuxt(config)
return new Builder(nuxt).build().catch((err) => {
const s = String(err)
expect(s).toContain('Style Resource not found')
})
await expect(new Builder(nuxt).build()).rejects.toThrow('Style Resource not found')
})
})

View File

@ -0,0 +1,30 @@
import consola from 'consola'
import { Nuxt } from '../utils'
const NO_BUILD_MSG = 'No build files found. Use either `nuxt build` or `builder.build()` or start nuxt in development mode.'
describe('renderer', () => {
test('detect no-build (Universal)', async () => {
const nuxt = new Nuxt({
_start: true,
mode: 'universal',
dev: false,
buildDir: '/path/to/404'
})
await nuxt.ready()
await expect(nuxt.renderer.renderer.isReady).toBe(false)
expect(consola.fatal).toHaveBeenCalledWith(new Error(NO_BUILD_MSG))
})
test('detect no-build (SPA)', async () => {
const nuxt = new Nuxt({
_start: true,
mode: 'spa',
dev: false,
buildDir: '/path/to/404'
})
await nuxt.ready()
await expect(nuxt.renderer.renderer.isReady).toBe(false)
expect(consola.fatal).toHaveBeenCalledWith(new Error(NO_BUILD_MSG))
})
})

View File

@ -1,6 +1,6 @@
import path from 'path'
import PerfLoader from '../../packages/webpack/src/config/utils/perf-loader'
import PerfLoader from '../../packages/webpack/src/utils/perf-loader'
describe('webpack configuration', () => {
test('performance loader', () => {

View File

@ -9845,7 +9845,7 @@ source-map-url@^0.4.0:
source-map@0.5.6:
version "0.5.6"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
resolved "http://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1: