mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-27 08:02:01 +00:00
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:
parent
06ddfbb77b
commit
0f104aa588
@ -72,7 +72,9 @@ export default class NuxtCommand {
|
|||||||
|
|
||||||
async getNuxt(options) {
|
async getNuxt(options) {
|
||||||
const { Nuxt } = await imports.core()
|
const { Nuxt } = await imports.core()
|
||||||
return new Nuxt(options)
|
const nuxt = new Nuxt(options)
|
||||||
|
await nuxt.ready()
|
||||||
|
return nuxt
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBuilder(nuxt) {
|
async getBuilder(nuxt) {
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
import consola from 'consola'
|
|
||||||
import { common, server } from '../options'
|
import { common, server } from '../options'
|
||||||
import { showBanner } from '../utils'
|
import { showBanner } from '../utils'
|
||||||
|
|
||||||
@ -17,32 +14,10 @@ export default {
|
|||||||
|
|
||||||
// Create production build when calling `nuxt build`
|
// Create production build when calling `nuxt build`
|
||||||
const nuxt = await cmd.getNuxt(
|
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
|
// Listen and show ready banner
|
||||||
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`'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nuxt.server.listen().then(() => {
|
return nuxt.server.listen().then(() => {
|
||||||
showBanner(nuxt)
|
showBanner(nuxt)
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs-extra'
|
||||||
import { consola, mockGetNuxtStart, mockGetNuxtConfig, NuxtCommand } from '../utils'
|
import { consola, mockGetNuxtStart, mockGetNuxtConfig, NuxtCommand } from '../utils'
|
||||||
|
|
||||||
describe('start', () => {
|
describe('start', () => {
|
||||||
@ -22,41 +22,14 @@ describe('start', () => {
|
|||||||
test('no error if dist dir exists', async () => {
|
test('no error if dist dir exists', async () => {
|
||||||
mockGetNuxtStart()
|
mockGetNuxtStart()
|
||||||
mockGetNuxtConfig()
|
mockGetNuxtConfig()
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => true)
|
|
||||||
|
|
||||||
await NuxtCommand.from(start).run()
|
await NuxtCommand.from(start).run()
|
||||||
|
|
||||||
expect(consola.fatal).not.toHaveBeenCalled()
|
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 () => {
|
test('no error on ssr and server bundle exists', async () => {
|
||||||
mockGetNuxtStart(true)
|
mockGetNuxtStart(true)
|
||||||
mockGetNuxtConfig()
|
mockGetNuxtConfig()
|
||||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => true)
|
|
||||||
|
|
||||||
await NuxtCommand.from(start).run()
|
await NuxtCommand.from(start).run()
|
||||||
|
|
||||||
expect(consola.fatal).not.toHaveBeenCalled()
|
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`')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -91,6 +91,7 @@ export const mockNuxt = (implementation) => {
|
|||||||
options: {},
|
options: {},
|
||||||
clearHook: jest.fn(),
|
clearHook: jest.fn(),
|
||||||
close: jest.fn(),
|
close: jest.fn(),
|
||||||
|
ready: jest.fn(),
|
||||||
server: {
|
server: {
|
||||||
listeners: [],
|
listeners: [],
|
||||||
listen: jest.fn().mockImplementationOnce(() => Promise.resolve())
|
listen: jest.fn().mockImplementationOnce(() => Promise.resolve())
|
||||||
|
@ -64,8 +64,7 @@ export default ({ resources, options }) => function errorMiddleware(err, req, re
|
|||||||
readSourceFactory({
|
readSourceFactory({
|
||||||
srcDir: options.srcDir,
|
srcDir: options.srcDir,
|
||||||
rootDir: options.rootDir,
|
rootDir: options.rootDir,
|
||||||
buildDir: options.buildDir,
|
buildDir: options.buildDir
|
||||||
resources
|
|
||||||
}),
|
}),
|
||||||
options.router.base,
|
options.router.base,
|
||||||
true
|
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
|
// Remove webpack:/// & query string from the end
|
||||||
const sanitizeName = name =>
|
const sanitizeName = name =>
|
||||||
name ? name.replace('webpack:///', '').split('?')[0] : null
|
name ? name.replace('webpack:///', '').split('?')[0] : null
|
||||||
@ -113,11 +112,4 @@ const readSourceFactory = ({ srcDir, rootDir, buildDir, resources }) => async fu
|
|||||||
return
|
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]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,14 @@
|
|||||||
],
|
],
|
||||||
"main": "dist/vue-app.js",
|
"main": "dist/vue-app.js",
|
||||||
"typings": "types/index.d.ts",
|
"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": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,7 @@
|
|||||||
"lru-cache": "^5.1.1",
|
"lru-cache": "^5.1.1",
|
||||||
"vue": "^2.5.17",
|
"vue": "^2.5.17",
|
||||||
"vue-meta": "^1.5.6",
|
"vue-meta": "^1.5.6",
|
||||||
"vue-no-ssr": "^1.1.0",
|
"vue-server-renderer": "^2.5.17"
|
||||||
"vue-router": "^3.0.2",
|
|
||||||
"vue-server-renderer": "^2.5.17",
|
|
||||||
"vue-template-compiler": "^2.5.17",
|
|
||||||
"vuex": "^3.0.1"
|
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import fs from 'fs-extra'
|
import fs from 'fs'
|
||||||
import consola from 'consola'
|
import consola from 'consola'
|
||||||
import devalue from '@nuxtjs/devalue'
|
import devalue from '@nuxtjs/devalue'
|
||||||
import invert from 'lodash/invert'
|
import invert from 'lodash/invert'
|
||||||
@ -16,19 +16,19 @@ export default class VueRenderer {
|
|||||||
|
|
||||||
// Will be set by createRenderer
|
// Will be set by createRenderer
|
||||||
this.renderer = {
|
this.renderer = {
|
||||||
ssr: null,
|
ssr: undefined,
|
||||||
modern: null,
|
modern: undefined,
|
||||||
spa: null
|
spa: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renderer runtime resources
|
// Renderer runtime resources
|
||||||
Object.assign(this.context.resources, {
|
Object.assign(this.context.resources, {
|
||||||
clientManifest: null,
|
clientManifest: undefined,
|
||||||
modernManifest: null,
|
modernManifest: undefined,
|
||||||
serverBundle: null,
|
serverManifest: undefined,
|
||||||
ssrTemplate: null,
|
ssrTemplate: undefined,
|
||||||
spaTemplate: null,
|
spaTemplate: undefined,
|
||||||
errorTemplate: this.constructor.parseTemplate('Nuxt.js Internal Server Error')
|
errorTemplate: this.parseTemplate('Nuxt.js Internal Server Error')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,57 +98,78 @@ export default class VueRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ready() {
|
async ready() {
|
||||||
// Production: Load SSR resources from fs
|
|
||||||
if (!this.context.options.dev) {
|
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 distPath = path.resolve(this.context.options.buildDir, 'dist', 'server')
|
||||||
const updated = []
|
const updated = []
|
||||||
|
const resourceMap = this.resourceMap
|
||||||
|
|
||||||
this.constructor.resourceMap.forEach(({ key, fileName, transform }) => {
|
const readResource = (fileName, encoding) => {
|
||||||
const rawKey = '$$' + key
|
try {
|
||||||
const _path = path.join(distPath, fileName)
|
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
|
for (const resourceName in resourceMap) {
|
||||||
if (!_fs.existsSync(_path)) {
|
const { fileName, transform, encoding } = resourceMap[resourceName]
|
||||||
// TODO: Enable baack when renderer initialzation was disabled for build only scripts
|
|
||||||
// Currently this breaks normal nuxt build for first time
|
// Load resource
|
||||||
// if (!this.context.options.dev) {
|
let resource = readResource(fileName, encoding)
|
||||||
// const invalidSSR = !this.noSSR && key === 'serverBundle'
|
|
||||||
// const invalidSPA = this.noSSR && key === 'spaTemplate'
|
// Skip unavailable resources
|
||||||
// if (invalidSPA || invalidSSR) {
|
if (!resource) {
|
||||||
// consola.fatal(`Could not load Nuxt renderer, make sure to build for production: builder.build() with dev option set to false.`)
|
consola.debug('Resource not available:', resourceName)
|
||||||
// }
|
continue
|
||||||
// }
|
|
||||||
return // Resource not exists
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawData = _fs.readFileSync(_path, 'utf8')
|
// Apply transforms
|
||||||
if (!rawData || rawData === this.context.resources[rawKey]) {
|
if (typeof transform === 'function') {
|
||||||
return // No changes
|
resource = transform(resource, {
|
||||||
|
readResource,
|
||||||
|
oldValue: this.context.resources[resourceName]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
this.context.resources[rawKey] = rawData
|
|
||||||
const data = transform(rawData)
|
// Update resource
|
||||||
/* istanbul ignore if */
|
this.context.resources[resourceName] = resource
|
||||||
if (!data) {
|
updated.push(resourceName)
|
||||||
return // Invalid data ?
|
}
|
||||||
}
|
|
||||||
this.context.resources[key] = data
|
|
||||||
updated.push(key)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reload error template
|
// Reload error template
|
||||||
const errorTemplatePath = path.resolve(this.context.options.buildDir, 'views/error.html')
|
const errorTemplatePath = path.resolve(this.context.options.buildDir, 'views/error.html')
|
||||||
if (fs.existsSync(errorTemplatePath)) {
|
if (fs.existsSync(errorTemplatePath)) {
|
||||||
this.context.resources.errorTemplate = this.constructor.parseTemplate(
|
this.context.resources.errorTemplate = this.parseTemplate(
|
||||||
fs.readFileSync(errorTemplatePath, 'utf8')
|
fs.readFileSync(errorTemplatePath, 'utf8')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loading template
|
// Reload loading template
|
||||||
const loadingHTMLPath = path.resolve(this.context.options.buildDir, 'loading.html')
|
const loadingHTMLPath = path.resolve(this.context.options.buildDir, 'loading.html')
|
||||||
if (fs.existsSync(loadingHTMLPath)) {
|
if (fs.existsSync(loadingHTMLPath)) {
|
||||||
this.context.resources.loadingHTML = fs.readFileSync(loadingHTMLPath, 'utf8')
|
this.context.resources.loadingHTML = fs.readFileSync(loadingHTMLPath, 'utf8')
|
||||||
@ -158,57 +179,60 @@ export default class VueRenderer {
|
|||||||
this.context.resources.loadingHTML = ''
|
this.context.resources.loadingHTML = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call resourcesLoaded plugin
|
// Call createRenderer if any resource changed
|
||||||
await this.context.nuxt.callHook('render:resourcesLoaded', this.context.resources)
|
|
||||||
|
|
||||||
if (updated.length > 0) {
|
if (updated.length > 0) {
|
||||||
this.createRenderer()
|
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
|
return this.context.options.render.ssr === false
|
||||||
}
|
}
|
||||||
|
|
||||||
get isReady() {
|
get SSR() {
|
||||||
if (this.noSSR) {
|
return this.context.options.render.ssr === true
|
||||||
return Boolean(this.context.resources.spaTemplate)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Boolean(this.renderer.ssr && this.context.resources.ssrTemplate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isResourcesAvailable() {
|
get isReady() {
|
||||||
// Required for both
|
// SPA
|
||||||
/* istanbul ignore if */
|
if (!this.context.resources.spaTemplate || !this.renderer.spa) {
|
||||||
if (!this.context.resources.clientManifest) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required for SPA rendering
|
// SSR
|
||||||
if (this.noSSR) {
|
if (this.SSR && (!this.context.resources.ssrTemplate || !this.renderer.ssr)) {
|
||||||
return Boolean(this.context.resources.spaTemplate)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required for bundle renderer
|
return true
|
||||||
return Boolean(this.context.resources.ssrTemplate && this.context.resources.serverBundle)
|
}
|
||||||
|
|
||||||
|
get isResourcesAvailable() { /* Backward compatibility */
|
||||||
|
return this.isReady
|
||||||
}
|
}
|
||||||
|
|
||||||
createRenderer() {
|
createRenderer() {
|
||||||
// Ensure resources are available
|
// Resource clientManifest is always required
|
||||||
if (!this.isResourcesAvailable) {
|
if (!this.context.resources.clientManifest) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Meta Renderer
|
// Create SPA renderer
|
||||||
this.renderer.spa = new SPAMetaRenderer(this)
|
if (this.context.resources.spaTemplate) {
|
||||||
|
this.renderer.spa = new SPAMetaRenderer(this)
|
||||||
|
}
|
||||||
|
|
||||||
// Skip following steps if noSSR mode
|
// Skip the rest if SSR resources are not available
|
||||||
if (this.noSSR) {
|
if (!this.context.resources.ssrTemplate || !this.context.resources.serverManifest) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasModules = fs.existsSync(path.resolve(this.context.options.rootDir, 'node_modules'))
|
const hasModules = fs.existsSync(path.resolve(this.context.options.rootDir, 'node_modules'))
|
||||||
|
|
||||||
const rendererOptions = {
|
const rendererOptions = {
|
||||||
runInNewContext: false,
|
runInNewContext: false,
|
||||||
clientManifest: this.context.resources.clientManifest,
|
clientManifest: this.context.resources.clientManifest,
|
||||||
@ -219,14 +243,14 @@ export default class VueRenderer {
|
|||||||
|
|
||||||
// Create bundle renderer for SSR
|
// Create bundle renderer for SSR
|
||||||
this.renderer.ssr = createBundleRenderer(
|
this.renderer.ssr = createBundleRenderer(
|
||||||
this.context.resources.serverBundle,
|
this.context.resources.serverManifest,
|
||||||
rendererOptions
|
rendererOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
if (this.context.resources.modernManifest &&
|
if (this.context.resources.modernManifest &&
|
||||||
!['client', false].includes(this.context.options.modern)) {
|
!['client', false].includes(this.context.options.modern)) {
|
||||||
this.renderer.modern = createBundleRenderer(
|
this.renderer.modern = createBundleRenderer(
|
||||||
this.context.resources.serverBundle,
|
this.context.resources.serverManifest,
|
||||||
{
|
{
|
||||||
...rendererOptions,
|
...rendererOptions,
|
||||||
clientManifest: this.context.resources.modernManifest
|
clientManifest: this.context.resources.modernManifest
|
||||||
@ -248,6 +272,7 @@ export default class VueRenderer {
|
|||||||
async renderRoute(url, context = {}) {
|
async renderRoute(url, context = {}) {
|
||||||
/* istanbul ignore if */
|
/* istanbul ignore if */
|
||||||
if (!this.isReady) {
|
if (!this.isReady) {
|
||||||
|
consola.info('Waiting for server resources...')
|
||||||
await waitFor(1000)
|
await waitFor(1000)
|
||||||
return this.renderRoute(url, context)
|
return this.renderRoute(url, context)
|
||||||
}
|
}
|
||||||
@ -258,12 +283,12 @@ export default class VueRenderer {
|
|||||||
// Add url and isSever to the context
|
// Add url and isSever to the context
|
||||||
context.url = url
|
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 { req, res } = context
|
||||||
const spa = context.spa || (res && res.spa)
|
const spa = context.spa || (res && res.spa)
|
||||||
const ENV = this.context.options.env
|
const ENV = this.context.options.env
|
||||||
|
|
||||||
if (this.noSSR || spa) {
|
if (!this.SSR || spa) {
|
||||||
const {
|
const {
|
||||||
HTML_ATTRS,
|
HTML_ATTRS,
|
||||||
BODY_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, {
|
return template(templateStr, {
|
||||||
interpolate: /{{([\s\S]+?)}}/g
|
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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import pify from 'pify'
|
import pify from 'pify'
|
||||||
import webpack from 'webpack'
|
import webpack from 'webpack'
|
||||||
@ -14,7 +13,8 @@ import {
|
|||||||
wrapArray
|
wrapArray
|
||||||
} from '@nuxt/common'
|
} 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)
|
const glob = pify(Glob)
|
||||||
|
|
||||||
@ -27,9 +27,11 @@ export class WebpackBundler {
|
|||||||
this.devMiddleware = {}
|
this.devMiddleware = {}
|
||||||
this.hotMiddleware = {}
|
this.hotMiddleware = {}
|
||||||
|
|
||||||
// Initialize shared FS and Cache
|
// Initialize shared MFS for dev
|
||||||
if (this.context.options.dev) {
|
if (this.context.options.dev) {
|
||||||
this.mfs = new MFS()
|
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'
|
'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) => {
|
await Promise.all(wrapArray(styleResources[ext]).map(async (p) => {
|
||||||
const styleResourceFiles = await glob(path.resolve(this.context.options.rootDir, 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}`)
|
throw new Error(`Style Resource not found: ${p}`)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
})
|
}
|
||||||
|
|
||||||
// Configure compilers
|
// Configure compilers
|
||||||
this.compilers = compilersOptions.map((compilersOption) => {
|
this.compilers = compilersOptions.map((compilersOption) => {
|
||||||
@ -135,7 +137,7 @@ export class WebpackBundler {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Reload renderer if available
|
// Reload renderer if available
|
||||||
nuxt.server.loadResources(this.mfs || fs)
|
await nuxt.callHook('build:resources', this.mfs)
|
||||||
|
|
||||||
// Resolve on next tick
|
// Resolve on next tick
|
||||||
process.nextTick(resolve)
|
process.nextTick(resolve)
|
||||||
|
@ -6,14 +6,15 @@ import cloneDeep from 'lodash/cloneDeep'
|
|||||||
import escapeRegExp from 'lodash/escapeRegExp'
|
import escapeRegExp from 'lodash/escapeRegExp'
|
||||||
import VueLoader from 'vue-loader'
|
import VueLoader from 'vue-loader'
|
||||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
|
||||||
|
import TerserWebpackPlugin from 'terser-webpack-plugin'
|
||||||
import WebpackBar from 'webpackbar'
|
import WebpackBar from 'webpackbar'
|
||||||
import env from 'std-env'
|
import env from 'std-env'
|
||||||
|
|
||||||
import { isUrl, urlJoin } from '@nuxt/common'
|
import { isUrl, urlJoin } from '@nuxt/common'
|
||||||
|
|
||||||
import PerfLoader from './utils/perf-loader'
|
import PerfLoader from '../utils/perf-loader'
|
||||||
import StyleLoader from './utils/style-loader'
|
import StyleLoader from '../utils/style-loader'
|
||||||
import WarnFixPlugin from './plugins/warnfix'
|
import WarnFixPlugin from '../plugins/warnfix'
|
||||||
|
|
||||||
export default class WebpackBaseConfig {
|
export default class WebpackBaseConfig {
|
||||||
constructor(builder, options) {
|
constructor(builder, options) {
|
||||||
@ -96,7 +97,7 @@ export default class WebpackBaseConfig {
|
|||||||
return fileName
|
return fileName
|
||||||
}
|
}
|
||||||
|
|
||||||
devtool() {
|
get devtool() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +128,41 @@ export default class WebpackBaseConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
optimization() {
|
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() {
|
alias() {
|
||||||
@ -337,10 +372,11 @@ export default class WebpackBaseConfig {
|
|||||||
config() {
|
config() {
|
||||||
// Prioritize nested node_modules in webpack search path (#2558)
|
// Prioritize nested node_modules in webpack search path (#2558)
|
||||||
const webpackModulesDir = ['node_modules'].concat(this.options.modulesDir)
|
const webpackModulesDir = ['node_modules'].concat(this.options.modulesDir)
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
mode: this.buildMode,
|
mode: this.buildMode,
|
||||||
devtool: this.devtool(),
|
devtool: this.devtool,
|
||||||
optimization: this.optimization(),
|
optimization: this.optimization(),
|
||||||
output: this.output(),
|
output: this.output(),
|
||||||
performance: {
|
performance: {
|
||||||
|
@ -2,12 +2,11 @@ import path from 'path'
|
|||||||
import webpack from 'webpack'
|
import webpack from 'webpack'
|
||||||
import HTMLPlugin from 'html-webpack-plugin'
|
import HTMLPlugin from 'html-webpack-plugin'
|
||||||
import BundleAnalyzer from 'webpack-bundle-analyzer'
|
import BundleAnalyzer from 'webpack-bundle-analyzer'
|
||||||
import TerserWebpackPlugin from 'terser-webpack-plugin'
|
|
||||||
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'
|
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'
|
||||||
import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin'
|
import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin'
|
||||||
|
|
||||||
import ModernModePlugin from './plugins/vue/modern'
|
import ModernModePlugin from '../plugins/vue/modern'
|
||||||
import VueSSRClientPlugin from './plugins/vue/client'
|
import VueSSRClientPlugin from '../plugins/vue/client'
|
||||||
import WebpackBaseConfig from './base'
|
import WebpackBaseConfig from './base'
|
||||||
|
|
||||||
export default class WebpackClientConfig extends WebpackBaseConfig {
|
export default class WebpackClientConfig extends WebpackBaseConfig {
|
||||||
@ -54,6 +53,21 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
|
|||||||
return optimization
|
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() {
|
plugins() {
|
||||||
const plugins = super.plugins()
|
const plugins = super.plugins()
|
||||||
|
|
||||||
@ -78,7 +92,7 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
|
|||||||
chunksSortMode: 'dependency'
|
chunksSortMode: 'dependency'
|
||||||
}),
|
}),
|
||||||
new VueSSRClientPlugin({
|
new VueSSRClientPlugin({
|
||||||
filename: `../server/vue-ssr-${this.name}-manifest.json`
|
filename: `../server/${this.name}.manifest.json`
|
||||||
}),
|
}),
|
||||||
new webpack.DefinePlugin(this.env())
|
new webpack.DefinePlugin(this.env())
|
||||||
)
|
)
|
||||||
@ -113,48 +127,6 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
|
|||||||
return plugins
|
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() {
|
config() {
|
||||||
const config = super.config()
|
const config = super.config()
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
export { default as ClientConfig } from './client'
|
export { default as ClientConfig } from './client'
|
||||||
export { default as ModernConfig } from './modern'
|
export { default as ModernConfig } from './modern'
|
||||||
export { default as ServerConfig } from './server'
|
export { default as ServerConfig } from './server'
|
||||||
export { default as PerfLoader } from './utils/perf-loader'
|
|
||||||
|
@ -4,8 +4,9 @@ import webpack from 'webpack'
|
|||||||
import escapeRegExp from 'lodash/escapeRegExp'
|
import escapeRegExp from 'lodash/escapeRegExp'
|
||||||
import nodeExternals from 'webpack-node-externals'
|
import nodeExternals from 'webpack-node-externals'
|
||||||
|
|
||||||
|
import VueSSRServerPlugin from '../plugins/vue/server'
|
||||||
|
|
||||||
import WebpackBaseConfig from './base'
|
import WebpackBaseConfig from './base'
|
||||||
import VueSSRServerPlugin from './plugins/vue/server'
|
|
||||||
|
|
||||||
export default class WebpackServerConfig extends WebpackBaseConfig {
|
export default class WebpackServerConfig extends WebpackBaseConfig {
|
||||||
constructor(builder) {
|
constructor(builder) {
|
||||||
@ -29,8 +30,8 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
|
|||||||
return whitelist
|
return whitelist
|
||||||
}
|
}
|
||||||
|
|
||||||
devtool() {
|
get devtool() {
|
||||||
return 'cheap-module-inline-source-map'
|
return 'cheap-module-source-map'
|
||||||
}
|
}
|
||||||
|
|
||||||
env() {
|
env() {
|
||||||
@ -45,7 +46,7 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
|
|||||||
optimization() {
|
optimization() {
|
||||||
return {
|
return {
|
||||||
splitChunks: false,
|
splitChunks: false,
|
||||||
minimizer: []
|
minimizer: this.minimizer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
|
|||||||
const plugins = super.plugins()
|
const plugins = super.plugins()
|
||||||
plugins.push(
|
plugins.push(
|
||||||
new VueSSRServerPlugin({
|
new VueSSRServerPlugin({
|
||||||
filename: 'server-bundle.json'
|
filename: `${this.name}.manifest.json`
|
||||||
}),
|
}),
|
||||||
new webpack.DefinePlugin(this.env())
|
new webpack.DefinePlugin(this.env())
|
||||||
)
|
)
|
||||||
@ -70,11 +71,12 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
|
|||||||
app: [path.resolve(this.options.buildDir, 'server.js')]
|
app: [path.resolve(this.options.buildDir, 'server.js')]
|
||||||
},
|
},
|
||||||
output: Object.assign({}, config.output, {
|
output: Object.assign({}, config.output, {
|
||||||
filename: 'server-bundle.js',
|
filename: 'server.js',
|
||||||
libraryTarget: 'commonjs2'
|
libraryTarget: 'commonjs2'
|
||||||
}),
|
}),
|
||||||
performance: {
|
performance: {
|
||||||
hints: false,
|
hints: false,
|
||||||
|
maxEntrypointSize: Infinity,
|
||||||
maxAssetSize: Infinity
|
maxAssetSize: Infinity
|
||||||
},
|
},
|
||||||
externals: []
|
externals: []
|
||||||
|
@ -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 hash from 'hash-sum'
|
||||||
import uniq from 'lodash/uniq'
|
import uniq from 'lodash/uniq'
|
||||||
|
|
||||||
@ -6,7 +11,7 @@ import { isJS, isCSS, onEmit } from './util'
|
|||||||
export default class VueSSRClientPlugin {
|
export default class VueSSRClientPlugin {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.options = Object.assign({
|
this.options = Object.assign({
|
||||||
filename: 'vue-ssr-client-manifest.json'
|
filename: null
|
||||||
}, options)
|
}, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +50,7 @@ export default class VueSSRClientPlugin {
|
|||||||
const assetModules = stats.modules.filter(m => m.assets.length)
|
const assetModules = stats.modules.filter(m => m.assets.length)
|
||||||
const fileToIndex = file => manifest.all.indexOf(file)
|
const fileToIndex = file => manifest.all.indexOf(file)
|
||||||
stats.modules.forEach((m) => {
|
stats.modules.forEach((m) => {
|
||||||
// ignore modules duplicated in multiple chunks
|
// Ignore modules duplicated in multiple chunks
|
||||||
if (m.chunks.length === 1) {
|
if (m.chunks.length === 1) {
|
||||||
const cid = m.chunks[0]
|
const cid = m.chunks[0]
|
||||||
const chunk = stats.chunks.find(c => c.id === cid)
|
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 id = m.identifier.replace(/\s\w+$/, '') // remove appended hash
|
||||||
const files = manifest.modules[hash(id)] = chunk.files.map(fileToIndex)
|
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) => {
|
assetModules.forEach((m) => {
|
||||||
if (m.chunks.some(id => id === cid)) {
|
if (m.chunks.some(id => id === cid)) {
|
||||||
files.push.apply(files, m.assets.map(fileToIndex))
|
files.push.apply(files, m.assets.map(fileToIndex))
|
||||||
@ -63,16 +69,11 @@ export default class VueSSRClientPlugin {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// const debug = (file, obj) => {
|
const src = JSON.stringify(manifest, null, 2)
|
||||||
// require('fs').writeFileSync(__dirname + '/' + file, JSON.stringify(obj, null, 2))
|
|
||||||
// }
|
|
||||||
// debug('stats.json', stats)
|
|
||||||
// debug('client-manifest.json', manifest)
|
|
||||||
|
|
||||||
const json = JSON.stringify(manifest, null, 2)
|
|
||||||
compilation.assets[this.options.filename] = {
|
compilation.assets[this.options.filename] = {
|
||||||
source: () => json,
|
source: () => src,
|
||||||
size: () => json.length
|
size: () => src.length
|
||||||
}
|
}
|
||||||
cb()
|
cb()
|
||||||
})
|
})
|
@ -1,16 +1,14 @@
|
|||||||
/*
|
/*
|
||||||
** This plugin is inspired by @vue/cli-service ModernModePlugin
|
* 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
|
* 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
|
import EventEmitter from 'events'
|
||||||
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()}}();`
|
|
||||||
|
|
||||||
const assetsMap = {}
|
const assetsMap = {}
|
||||||
const watcher = new EventEmitter()
|
const watcher = new EventEmitter()
|
||||||
|
|
||||||
class ModernModePlugin {
|
export default class ModernModePlugin {
|
||||||
constructor({ targetDir, isModernBuild }) {
|
constructor({ targetDir, isModernBuild }) {
|
||||||
this.targetDir = targetDir
|
this.targetDir = targetDir
|
||||||
this.isModernBuild = isModernBuild
|
this.isModernBuild = isModernBuild
|
||||||
@ -83,7 +81,7 @@ class ModernModePlugin {
|
|||||||
data.body.push({
|
data.body.push({
|
||||||
tagName: 'script',
|
tagName: 'script',
|
||||||
closeTag: true,
|
closeTag: true,
|
||||||
innerHTML: safariFix
|
innerHTML: ModernModePlugin.safariFix
|
||||||
})
|
})
|
||||||
|
|
||||||
// inject links for legacy assets as <script nomodule>
|
// inject links for legacy assets as <script nomodule>
|
||||||
@ -107,6 +105,5 @@ class ModernModePlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ModernModePlugin.safariFix = safariFix
|
// 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()}}();`
|
||||||
export default ModernModePlugin
|
|
@ -3,7 +3,7 @@ import { validate, isJS, onEmit } from './util'
|
|||||||
export default class VueSSRServerPlugin {
|
export default class VueSSRServerPlugin {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.options = Object.assign({
|
this.options = Object.assign({
|
||||||
filename: 'vue-ssr-server-bundle.json'
|
filename: null
|
||||||
}, options)
|
}, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,20 +44,17 @@ export default class VueSSRServerPlugin {
|
|||||||
|
|
||||||
stats.assets.forEach((asset) => {
|
stats.assets.forEach((asset) => {
|
||||||
if (isJS(asset.name)) {
|
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$/)) {
|
} 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 src = JSON.stringify(bundle, null, 2)
|
||||||
const filename = this.options.filename
|
|
||||||
|
|
||||||
compilation.assets[filename] = {
|
compilation.assets[this.options.filename] = {
|
||||||
source: () => json,
|
source: () => src,
|
||||||
size: () => json.length
|
size: () => src.length
|
||||||
}
|
}
|
||||||
|
|
||||||
cb()
|
cb()
|
@ -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]`
|
import consola from 'consola'
|
||||||
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
|
|
||||||
|
|
||||||
export const validate = (compiler) => {
|
export const validate = (compiler) => {
|
||||||
if (compiler.options.target !== 'node') {
|
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') {
|
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) {
|
if (!compiler.options.externals) {
|
||||||
tip(
|
consola.info(
|
||||||
'It is recommended to externalize dependencies in the server build for ' +
|
'It is recommended to externalize dependencies in the server build for ' +
|
||||||
'better build performance.'
|
'better build performance.'
|
||||||
)
|
)
|
@ -1,5 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
styleResources: {
|
build: {
|
||||||
'stylus': './nothinghere'
|
styleResources: {
|
||||||
|
'stylus': './nothinghere'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 () => {
|
test('/error should return json format error (Youch)', async () => {
|
||||||
const opts = {
|
const opts = {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -282,11 +282,6 @@ describe('basic ssr', () => {
|
|||||||
.rejects.toMatchObject({ statusCode: 304 })
|
.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 () => {
|
test('/_nuxt/ should return 404', async () => {
|
||||||
await expect(rp(url('/_nuxt/')))
|
await expect(rp(url('/_nuxt/')))
|
||||||
.rejects.toMatchObject({ statusCode: 404 })
|
.rejects.toMatchObject({ statusCode: 404 })
|
||||||
|
@ -19,22 +19,25 @@ describe('nuxt', () => {
|
|||||||
expect(nuxt.initialized).toBe(true)
|
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({
|
const nuxt = new Nuxt({
|
||||||
dev: false,
|
...config,
|
||||||
rootDir: resolve(__dirname, '..', 'fixtures', 'empty', 'pages')
|
rootDir: resolve(__dirname, '..', 'fixtures', 'empty', 'pages')
|
||||||
})
|
})
|
||||||
|
|
||||||
return new Builder(nuxt).build().catch((err) => {
|
try {
|
||||||
const s = String(err)
|
await new Builder(nuxt).build()
|
||||||
expect(s).toContain('No `pages` directory found')
|
} catch (err) {
|
||||||
expect(s).toContain('Did you mean to run `nuxt` in the parent (`../`) directory?')
|
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 () => {
|
test('Build with default page when no pages/ directory', async () => {
|
||||||
const nuxt = new Nuxt()
|
const config = await loadFixture('missing-pages-dir')
|
||||||
new Builder(nuxt).build()
|
const nuxt = new Nuxt(config)
|
||||||
const port = await getPort()
|
const port = await getPort()
|
||||||
await nuxt.server.listen(port, 'localhost')
|
await nuxt.server.listen(port, 'localhost')
|
||||||
|
|
||||||
@ -45,27 +48,17 @@ describe('nuxt', () => {
|
|||||||
await nuxt.close()
|
await nuxt.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Fail to build when specified plugin isn\'t found', () => {
|
test('Fail to build when specified plugin isn\'t found', async () => {
|
||||||
const nuxt = new Nuxt({
|
const config = await loadFixture('missing-plugin')
|
||||||
dev: false,
|
const nuxt = new Nuxt(config)
|
||||||
rootDir: resolve(__dirname, '..', 'fixtures', 'missing-plugin')
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Builder(nuxt).build().catch((err) => {
|
await expect(new Builder(nuxt).build()).rejects.toThrow('Plugin not found')
|
||||||
const s = String(err)
|
|
||||||
expect(s).toContain('Plugin not found')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Warn when styleResource isn\'t found', () => {
|
test('Warn when styleResource isn\'t found', async () => {
|
||||||
const nuxt = new Nuxt({
|
const config = await loadFixture('missing-style-resource')
|
||||||
dev: false,
|
const nuxt = new Nuxt(config)
|
||||||
rootDir: resolve(__dirname, '..', 'fixtures', 'missing-style-resource')
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Builder(nuxt).build().catch((err) => {
|
await expect(new Builder(nuxt).build()).rejects.toThrow('Style Resource not found')
|
||||||
const s = String(err)
|
|
||||||
expect(s).toContain('Style Resource not found')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
30
test/unit/renderer.test.js
Normal file
30
test/unit/renderer.test.js
Normal 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))
|
||||||
|
})
|
||||||
|
})
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import path from 'path'
|
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', () => {
|
describe('webpack configuration', () => {
|
||||||
test('performance loader', () => {
|
test('performance loader', () => {
|
||||||
|
@ -9845,7 +9845,7 @@ source-map-url@^0.4.0:
|
|||||||
|
|
||||||
source-map@0.5.6:
|
source-map@0.5.6:
|
||||||
version "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=
|
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:
|
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:
|
||||||
|
Loading…
Reference in New Issue
Block a user