mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-27 08:02:01 +00:00
feat: client mode modern and support dev/generate (#4264)
This commit is contained in:
parent
83f8b1183c
commit
2a36dbad22
@ -24,17 +24,6 @@ export default {
|
|||||||
default: true,
|
default: true,
|
||||||
description: 'Don\'t generate static version for SPA mode (useful for nuxt start)'
|
description: 'Don\'t generate static version for SPA mode (useful for nuxt start)'
|
||||||
},
|
},
|
||||||
modern: {
|
|
||||||
alias: 'm',
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Build app for modern browsers',
|
|
||||||
prepare(cmd, options, argv) {
|
|
||||||
options.build = options.build || {}
|
|
||||||
if (argv.modern) {
|
|
||||||
options.build.modern = !!argv.modern
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
quiet: {
|
quiet: {
|
||||||
alias: 'q',
|
alias: 'q',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
@ -11,6 +11,15 @@ export default {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
description: 'Only generate pages for dynamic routes. Nuxt has to be built once before using this option'
|
description: 'Only generate pages for dynamic routes. Nuxt has to be built once before using this option'
|
||||||
|
},
|
||||||
|
modern: {
|
||||||
|
...common.modern,
|
||||||
|
description: 'Generate app in modern build (modern mode can be only client)',
|
||||||
|
prepare(cmd, options, argv) {
|
||||||
|
if (argv.modern) {
|
||||||
|
options.modern = 'client'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async run(cmd) {
|
async run(cmd) {
|
||||||
|
@ -9,18 +9,7 @@ export default {
|
|||||||
usage: 'start <dir>',
|
usage: 'start <dir>',
|
||||||
options: {
|
options: {
|
||||||
...common,
|
...common,
|
||||||
...server,
|
...server
|
||||||
modern: {
|
|
||||||
alias: 'm',
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Build app for modern browsers',
|
|
||||||
prepare(cmd, options, argv) {
|
|
||||||
options.build = options.build || {}
|
|
||||||
if (argv.modern) {
|
|
||||||
options.build.modern = !!argv.modern
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async run(cmd) {
|
async run(cmd) {
|
||||||
const argv = cmd.getArgv()
|
const argv = cmd.getArgv()
|
||||||
|
@ -15,6 +15,16 @@ export default {
|
|||||||
default: 'nuxt.config.js',
|
default: 'nuxt.config.js',
|
||||||
description: 'Path to Nuxt.js config file (default: nuxt.config.js)'
|
description: 'Path to Nuxt.js config file (default: nuxt.config.js)'
|
||||||
},
|
},
|
||||||
|
modern: {
|
||||||
|
alias: 'm',
|
||||||
|
type: 'string',
|
||||||
|
description: 'Build/Start app for modern browsers, e.g. server, client and false',
|
||||||
|
prepare(cmd, options, argv) {
|
||||||
|
if (argv.modern) {
|
||||||
|
options.modern = argv.modern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
version: {
|
version: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Display the Nuxt version'
|
description: 'Display the Nuxt version'
|
||||||
|
@ -10,6 +10,7 @@ exports[`cli/command builds help text 1`] = `
|
|||||||
--spa, -s Launch in SPA mode
|
--spa, -s Launch in SPA mode
|
||||||
--universal, -u Launch in Universal mode (default)
|
--universal, -u Launch in Universal mode (default)
|
||||||
--config-file, -c Path to Nuxt.js config file (default: nuxt.config.js)
|
--config-file, -c Path to Nuxt.js config file (default: nuxt.config.js)
|
||||||
|
--modern, -m Build/Start app for modern browsers, e.g. server, client and false
|
||||||
--version Display the Nuxt version
|
--version Display the Nuxt version
|
||||||
--help, -h Display this message
|
--help, -h Display this message
|
||||||
--port, -p Port number on which to start the application
|
--port, -p Port number on which to start the application
|
||||||
@ -30,6 +31,7 @@ exports[`cli/command loads command from name 1`] = `
|
|||||||
--spa, -s Launch in SPA mode
|
--spa, -s Launch in SPA mode
|
||||||
--universal, -u Launch in Universal mode (default)
|
--universal, -u Launch in Universal mode (default)
|
||||||
--config-file, -c Path to Nuxt.js config file (default: nuxt.config.js)
|
--config-file, -c Path to Nuxt.js config file (default: nuxt.config.js)
|
||||||
|
--modern, -m Build/Start app for modern browsers, e.g. server, client and false
|
||||||
--version Display the Nuxt version
|
--version Display the Nuxt version
|
||||||
--help, -h Display this message
|
--help, -h Display this message
|
||||||
--port, -p Port number on which to start the application
|
--port, -p Port number on which to start the application
|
||||||
|
@ -18,7 +18,7 @@ describe('cli/command', () => {
|
|||||||
const cmd = new Command({ options: allOptions })
|
const cmd = new Command({ options: allOptions })
|
||||||
const minimistOptions = cmd._getMinimistOptions()
|
const minimistOptions = cmd._getMinimistOptions()
|
||||||
|
|
||||||
expect(minimistOptions.string.length).toBe(4)
|
expect(minimistOptions.string.length).toBe(5)
|
||||||
expect(minimistOptions.boolean.length).toBe(4)
|
expect(minimistOptions.boolean.length).toBe(4)
|
||||||
expect(minimistOptions.alias.c).toBe('config-file')
|
expect(minimistOptions.alias.c).toBe('config-file')
|
||||||
expect(minimistOptions.default.c).toBe(common['config-file'].default)
|
expect(minimistOptions.default.c).toBe(common['config-file'].default)
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export { default as Hookable } from './hookable'
|
export { default as Hookable } from './hookable'
|
||||||
|
export { default as ModernBrowsers } from '../data/modern-browsers.json'
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
|
@ -12,6 +12,7 @@ export default () => ({
|
|||||||
|
|
||||||
// Mode
|
// Mode
|
||||||
mode: 'universal',
|
mode: 'universal',
|
||||||
|
modern: false,
|
||||||
|
|
||||||
// Globals
|
// Globals
|
||||||
globalName: `nuxt`,
|
globalName: `nuxt`,
|
||||||
|
@ -7,7 +7,6 @@ export default () => ({
|
|||||||
extractCSS: false,
|
extractCSS: false,
|
||||||
cssSourceMap: undefined,
|
cssSourceMap: undefined,
|
||||||
ssr: undefined,
|
ssr: undefined,
|
||||||
modern: undefined,
|
|
||||||
parallel: false,
|
parallel: false,
|
||||||
cache: false,
|
cache: false,
|
||||||
publicPath: '/_nuxt/',
|
publicPath: '/_nuxt/',
|
||||||
|
@ -216,6 +216,10 @@ export function getNuxtConfig(_options) {
|
|||||||
const modePreset = options.modes[options.mode || 'universal']
|
const modePreset = options.modes[options.mode || 'universal']
|
||||||
defaultsDeep(options, modePreset)
|
defaultsDeep(options, modePreset)
|
||||||
|
|
||||||
|
if (options.modern === true) {
|
||||||
|
options.modern = 'server'
|
||||||
|
}
|
||||||
|
|
||||||
// If no server-side rendering, add appear true transition
|
// If no server-side rendering, add appear true transition
|
||||||
/* istanbul ignore if */
|
/* istanbul ignore if */
|
||||||
if (options.render.ssr === false && options.transition) {
|
if (options.render.ssr === false && options.transition) {
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"@nuxt/common": "^2.2.0",
|
"@nuxt/common": "^2.2.0",
|
||||||
"@nuxt/config": "^2.2.0",
|
"@nuxt/config": "^2.2.0",
|
||||||
"@nuxtjs/youch": "^4.2.3",
|
"@nuxtjs/youch": "^4.2.3",
|
||||||
|
"browserslist-useragent": "^2.0.1",
|
||||||
"chalk": "^2.4.1",
|
"chalk": "^2.4.1",
|
||||||
"compression": "^1.7.3",
|
"compression": "^1.7.3",
|
||||||
"connect": "^3.6.6",
|
"connect": "^3.6.6",
|
||||||
|
22
packages/server/src/middleware/modern.js
Normal file
22
packages/server/src/middleware/modern.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ModernBrowsers } from '@nuxt/common'
|
||||||
|
import { matchesUA } from 'browserslist-useragent'
|
||||||
|
|
||||||
|
const modernBrowsers = Object.keys(ModernBrowsers)
|
||||||
|
.map(browser => `${browser} >= ${ModernBrowsers[browser]}`)
|
||||||
|
|
||||||
|
const isModernBrowser = (ua) => {
|
||||||
|
return Boolean(ua) && matchesUA(ua, {
|
||||||
|
allowHigherVersions: true,
|
||||||
|
browsers: modernBrowsers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (req, res, next) {
|
||||||
|
const { socket = {}, headers } = req
|
||||||
|
if (socket.isModernBrowser === undefined) {
|
||||||
|
const ua = headers && headers['user-agent']
|
||||||
|
socket.isModernBrowser = isModernBrowser(ua)
|
||||||
|
}
|
||||||
|
req.isModernBrowser = socket.isModernBrowser
|
||||||
|
next()
|
||||||
|
}
|
@ -13,6 +13,7 @@ import ServerContext from './context'
|
|||||||
import renderAndGetWindow from './jsdom'
|
import renderAndGetWindow from './jsdom'
|
||||||
import nuxtMiddleware from './middleware/nuxt'
|
import nuxtMiddleware from './middleware/nuxt'
|
||||||
import errorMiddleware from './middleware/error'
|
import errorMiddleware from './middleware/error'
|
||||||
|
import modernMiddleware from './middleware/modern'
|
||||||
|
|
||||||
export default class Server {
|
export default class Server {
|
||||||
constructor(nuxt) {
|
constructor(nuxt) {
|
||||||
@ -29,8 +30,8 @@ export default class Server {
|
|||||||
this.resources = {}
|
this.resources = {}
|
||||||
|
|
||||||
// Will be available on dev
|
// Will be available on dev
|
||||||
this.webpackDevMiddleware = null
|
this.devMiddleware = null
|
||||||
this.webpackHotMiddleware = null
|
this.hotMiddleware = null
|
||||||
|
|
||||||
// Create new connect instance
|
// Create new connect instance
|
||||||
this.app = connect()
|
this.app = connect()
|
||||||
@ -70,14 +71,19 @@ export default class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.modern === 'server') {
|
||||||
|
this.useMiddleware(modernMiddleware)
|
||||||
|
}
|
||||||
|
|
||||||
// Add webpack middleware support only for development
|
// Add webpack middleware support only for development
|
||||||
if (this.options.dev) {
|
if (this.options.dev) {
|
||||||
this.useMiddleware(async (req, res, next) => {
|
this.useMiddleware(async (req, res, next) => {
|
||||||
if (this.webpackDevMiddleware) {
|
const name = req.isModernBrowser ? 'modern' : 'client'
|
||||||
await this.webpackDevMiddleware(req, res)
|
if (this.devMiddleware[name]) {
|
||||||
|
await this.devMiddleware[name](req, res)
|
||||||
}
|
}
|
||||||
if (this.webpackHotMiddleware) {
|
if (this.hotMiddleware[name]) {
|
||||||
await this.webpackHotMiddleware(req, res)
|
await this.hotMiddleware[name](req, res)
|
||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/common": "^2.2.0",
|
"@nuxt/common": "^2.2.0",
|
||||||
"@nuxtjs/devalue": "^1.1.0",
|
"@nuxtjs/devalue": "^1.1.0",
|
||||||
"browserslist-useragent": "^2.0.1",
|
|
||||||
"consola": "^2.2.2",
|
"consola": "^2.2.2",
|
||||||
"fs-extra": "^7.0.1",
|
"fs-extra": "^7.0.1",
|
||||||
"lru-cache": "^4.1.3",
|
"lru-cache": "^4.1.3",
|
||||||
|
@ -3,12 +3,11 @@ import crypto from 'crypto'
|
|||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
import consola from 'consola'
|
import consola from 'consola'
|
||||||
import devalue from '@nuxtjs/devalue'
|
import devalue from '@nuxtjs/devalue'
|
||||||
|
import invert from 'lodash/invert'
|
||||||
import template from 'lodash/template'
|
import template from 'lodash/template'
|
||||||
import { waitFor } from '@nuxt/common'
|
import { waitFor } from '@nuxt/common'
|
||||||
import { matchesUA } from 'browserslist-useragent'
|
|
||||||
import { createBundleRenderer } from 'vue-server-renderer'
|
import { createBundleRenderer } from 'vue-server-renderer'
|
||||||
|
|
||||||
import ModernBrowsers from '../data/modern-browsers.json'
|
|
||||||
import SPAMetaRenderer from './spa-meta'
|
import SPAMetaRenderer from './spa-meta'
|
||||||
|
|
||||||
export default class VueRenderer {
|
export default class VueRenderer {
|
||||||
@ -22,9 +21,6 @@ export default class VueRenderer {
|
|||||||
spa: null
|
spa: null
|
||||||
}
|
}
|
||||||
|
|
||||||
this.modernBrowsers = Object.keys(ModernBrowsers)
|
|
||||||
.map(browser => `${browser} >= ${ModernBrowsers[browser]}`)
|
|
||||||
|
|
||||||
// Renderer runtime resources
|
// Renderer runtime resources
|
||||||
Object.assign(this.context.resources, {
|
Object.assign(this.context.resources, {
|
||||||
clientManifest: null,
|
clientManifest: null,
|
||||||
@ -36,6 +32,52 @@ export default class VueRenderer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get assetsMapping() {
|
||||||
|
if (this._assetsMapping) return this._assetsMapping
|
||||||
|
|
||||||
|
const legacyAssets = this.context.resources.clientManifest.assetsMapping
|
||||||
|
const modernAssets = invert(this.context.resources.modernManifest.assetsMapping)
|
||||||
|
const mapping = {}
|
||||||
|
for (const legacyJsFile in legacyAssets) {
|
||||||
|
const chunkNamesHash = legacyAssets[legacyJsFile]
|
||||||
|
mapping[legacyJsFile] = modernAssets[chunkNamesHash]
|
||||||
|
}
|
||||||
|
delete this.context.resources.clientManifest.assetsMapping
|
||||||
|
delete this.context.resources.modernManifest.assetsMapping
|
||||||
|
this._assetsMapping = mapping
|
||||||
|
return mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
renderScripts(context) {
|
||||||
|
if (this.context.options.modern === 'client') {
|
||||||
|
const publicPath = this.context.options.build.publicPath
|
||||||
|
const scriptPattern = /<script[^>]*?src="([^"]*)"[^>]*>[^<]*<\/script>/g
|
||||||
|
return context.renderScripts().replace(scriptPattern, (scriptTag, jsFile) => {
|
||||||
|
const legacyJsFile = jsFile.replace(publicPath, '')
|
||||||
|
const modernJsFile = this.assetsMapping[legacyJsFile]
|
||||||
|
const moduleTag = scriptTag.replace('<script', '<script type="module"').replace(legacyJsFile, modernJsFile)
|
||||||
|
const noModuleTag = scriptTag.replace('<script', '<script nomodule')
|
||||||
|
return noModuleTag + moduleTag
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return context.renderScripts()
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResourceHints(context) {
|
||||||
|
if (this.context.options.modern === 'client') {
|
||||||
|
const modulePreloadTags = []
|
||||||
|
for (const legacyJsFile of context.getPreloadFiles()) {
|
||||||
|
if (legacyJsFile.asType === 'script') {
|
||||||
|
const publicPath = this.context.options.build.publicPath
|
||||||
|
const modernJsFile = this.assetsMapping[legacyJsFile.file]
|
||||||
|
modulePreloadTags.push(`<link rel="modulepreload" href="${publicPath}${modernJsFile}" as="script">`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modulePreloadTags.join('')
|
||||||
|
}
|
||||||
|
return context.renderResourceHints()
|
||||||
|
}
|
||||||
|
|
||||||
async ready() {
|
async ready() {
|
||||||
// Production: Load SSR resources from fs
|
// Production: Load SSR resources from fs
|
||||||
if (!this.context.options.dev) {
|
if (!this.context.options.dev) {
|
||||||
@ -151,7 +193,7 @@ export default class VueRenderer {
|
|||||||
rendererOptions
|
rendererOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
if (this.context.options.build.modern) {
|
if (this.context.options.modern === 'server') {
|
||||||
this.renderer.modern = createBundleRenderer(
|
this.renderer.modern = createBundleRenderer(
|
||||||
this.context.resources.serverBundle,
|
this.context.resources.serverBundle,
|
||||||
{
|
{
|
||||||
@ -186,7 +228,8 @@ export default class VueRenderer {
|
|||||||
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 spa = context.spa || (context.res && context.res.spa)
|
const { req, res } = context
|
||||||
|
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.noSSR || spa) {
|
||||||
@ -224,18 +267,9 @@ export default class VueRenderer {
|
|||||||
return { html, getPreloadFiles }
|
return { html, getPreloadFiles }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { req: { socket = {}, headers } = {} } = context
|
|
||||||
if (socket.isModernBrowser === undefined) {
|
|
||||||
const ua = headers && headers['user-agent']
|
|
||||||
socket.isModernBrowser = this.renderer.modern && ua && matchesUA(ua, {
|
|
||||||
allowHigherVersions: true,
|
|
||||||
browsers: this.modernBrowsers
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let APP
|
let APP
|
||||||
// Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
|
// Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
|
||||||
if (socket.isModernBrowser) {
|
if (req && req.isModernBrowser) {
|
||||||
APP = await this.renderer.modern.renderToString(context)
|
APP = await this.renderer.modern.renderToString(context)
|
||||||
} else {
|
} else {
|
||||||
APP = await this.renderer.ssr.renderToString(context)
|
APP = await this.renderer.ssr.renderToString(context)
|
||||||
@ -257,7 +291,7 @@ export default class VueRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.context.options.render.resourceHints) {
|
if (this.context.options.render.resourceHints) {
|
||||||
HEAD += context.renderResourceHints()
|
HEAD += this.renderResourceHints(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.context.nuxt.callHook('render:routeContext', context.nuxt)
|
await this.context.nuxt.callHook('render:routeContext', context.nuxt)
|
||||||
@ -273,7 +307,7 @@ export default class VueRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
APP += `<script>${serializedSession}</script>`
|
APP += `<script>${serializedSession}</script>`
|
||||||
APP += context.renderScripts()
|
APP += this.renderScripts(context)
|
||||||
APP += m.script.text({ body: true })
|
APP += m.script.text({ body: true })
|
||||||
APP += m.noscript.text({ body: true })
|
APP += m.noscript.text({ body: true })
|
||||||
|
|
||||||
|
@ -24,8 +24,8 @@ export class WebpackBuilder {
|
|||||||
// Fields that set on build
|
// Fields that set on build
|
||||||
this.compilers = []
|
this.compilers = []
|
||||||
this.compilersWatching = []
|
this.compilersWatching = []
|
||||||
this.webpackDevMiddleware = null
|
this.devMiddleware = {}
|
||||||
this.webpackHotMiddleware = null
|
this.hotMiddleware = {}
|
||||||
this.perfLoader = null
|
this.perfLoader = null
|
||||||
|
|
||||||
// Initialize shared FS and Cache
|
// Initialize shared FS and Cache
|
||||||
@ -47,7 +47,7 @@ export class WebpackBuilder {
|
|||||||
|
|
||||||
// Modern
|
// Modern
|
||||||
let modernConfig
|
let modernConfig
|
||||||
if (options.build.modern) {
|
if (options.modern) {
|
||||||
modernConfig = new ModernConfig(this).config()
|
modernConfig = new ModernConfig(this).config()
|
||||||
compilersOptions.push(modernConfig)
|
compilersOptions.push(modernConfig)
|
||||||
}
|
}
|
||||||
@ -141,7 +141,7 @@ export class WebpackBuilder {
|
|||||||
if (options.dev) {
|
if (options.dev) {
|
||||||
// --- Dev Build ---
|
// --- Dev Build ---
|
||||||
// Client Build, watch is started by dev-middleware
|
// Client Build, watch is started by dev-middleware
|
||||||
if (compiler.options.name === 'client') {
|
if (['client', 'modern'].includes(name)) {
|
||||||
return this.webpackDev(compiler)
|
return this.webpackDev(compiler)
|
||||||
}
|
}
|
||||||
// Server, build and watch for changes
|
// Server, build and watch for changes
|
||||||
@ -178,10 +178,11 @@ export class WebpackBuilder {
|
|||||||
webpackDev(compiler) {
|
webpackDev(compiler) {
|
||||||
consola.debug('Adding webpack middleware...')
|
consola.debug('Adding webpack middleware...')
|
||||||
|
|
||||||
|
const name = [compiler.options.name]
|
||||||
const { nuxt: { server }, options } = this.context
|
const { nuxt: { server }, options } = this.context
|
||||||
|
|
||||||
// Create webpack dev middleware
|
// Create webpack dev middleware
|
||||||
this.webpackDevMiddleware = pify(
|
this.devMiddleware[name] = pify(
|
||||||
webpackDevMiddleware(
|
webpackDevMiddleware(
|
||||||
compiler,
|
compiler,
|
||||||
Object.assign(
|
Object.assign(
|
||||||
@ -196,9 +197,9 @@ export class WebpackBuilder {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
this.webpackDevMiddleware.close = pify(this.webpackDevMiddleware.close)
|
this.devMiddleware[name].close = pify(this.devMiddleware[name].close)
|
||||||
|
|
||||||
this.webpackHotMiddleware = pify(
|
this.hotMiddleware[name] = pify(
|
||||||
webpackHotMiddleware(
|
webpackHotMiddleware(
|
||||||
compiler,
|
compiler,
|
||||||
Object.assign(
|
Object.assign(
|
||||||
@ -206,15 +207,18 @@ export class WebpackBuilder {
|
|||||||
log: false,
|
log: false,
|
||||||
heartbeat: 10000
|
heartbeat: 10000
|
||||||
},
|
},
|
||||||
options.build.hotMiddleware
|
options.build.hotMiddleware,
|
||||||
|
{
|
||||||
|
path: `/__webpack_hmr/${name}`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Inject to renderer instance
|
// Inject to renderer instance
|
||||||
if (server) {
|
if (server) {
|
||||||
server.webpackDevMiddleware = this.webpackDevMiddleware
|
server.devMiddleware = this.devMiddleware
|
||||||
server.webpackHotMiddleware = this.webpackHotMiddleware
|
server.hotMiddleware = this.hotMiddleware
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,8 +227,8 @@ export class WebpackBuilder {
|
|||||||
watching.close()
|
watching.close()
|
||||||
}
|
}
|
||||||
// Stop webpack middleware
|
// Stop webpack middleware
|
||||||
if (this.webpackDevMiddleware) {
|
for (const devMiddleware of Object.values(this.devMiddleware)) {
|
||||||
await this.webpackDevMiddleware.close()
|
await devMiddleware.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
|
|||||||
}, this.options.build.analyze)))
|
}, this.options.build.analyze)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.build.modern) {
|
if (this.options.modern) {
|
||||||
plugins.push(new ModernModePlugin({
|
plugins.push(new ModernModePlugin({
|
||||||
targetDir: path.resolve(this.options.buildDir, 'dist', 'client'),
|
targetDir: path.resolve(this.options.buildDir, 'dist', 'client'),
|
||||||
isModernBuild: this.isModern
|
isModernBuild: this.isModern
|
||||||
@ -167,9 +167,9 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
|
|||||||
if (this.options.dev) {
|
if (this.options.dev) {
|
||||||
config.entry.app.unshift(
|
config.entry.app.unshift(
|
||||||
// https://github.com/glenjamin/webpack-hot-middleware#config
|
// https://github.com/glenjamin/webpack-hot-middleware#config
|
||||||
`webpack-hot-middleware/client?name=client&reload=true&timeout=30000&path=${
|
`webpack-hot-middleware/client?name=${this.name}&reload=true&timeout=30000&path=${
|
||||||
this.options.router.base
|
this.options.router.base
|
||||||
}/__webpack_hmr`.replace(/\/\//g, '/')
|
}/__webpack_hmr/${this.name}`.replace(/\/\//g, '/')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,12 +26,20 @@ export default class VueSSRClientPlugin {
|
|||||||
.filter(file => isJS(file) || isCSS(file))
|
.filter(file => isJS(file) || isCSS(file))
|
||||||
.filter(file => !initialFiles.includes(file))
|
.filter(file => !initialFiles.includes(file))
|
||||||
|
|
||||||
|
const assetsMapping = {}
|
||||||
|
stats.assets
|
||||||
|
.filter(({ name }) => isJS(name))
|
||||||
|
.forEach(({ name, chunkNames }) => {
|
||||||
|
assetsMapping[name] = hash(chunkNames.join('|'))
|
||||||
|
})
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
publicPath: stats.publicPath,
|
publicPath: stats.publicPath,
|
||||||
all: allFiles,
|
all: allFiles,
|
||||||
initial: initialFiles,
|
initial: initialFiles,
|
||||||
async: asyncFiles,
|
async: asyncFiles,
|
||||||
modules: { /* [identifier: string]: Array<index: number> */ }
|
modules: { /* [identifier: string]: Array<index: number> */ },
|
||||||
|
assetsMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetModules = stats.modules.filter(m => m.assets.length)
|
const assetModules = stats.modules.filter(m => m.assets.length)
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
** This plugin is inspired by @vue/cli-service ModernModePlugin
|
** 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
|
** https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js
|
||||||
*/
|
*/
|
||||||
import path from 'path'
|
import EventEmitter from 'events'
|
||||||
import fs from 'fs-extra'
|
|
||||||
|
|
||||||
// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
|
// 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()}}();`
|
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 watcher = new EventEmitter()
|
||||||
|
|
||||||
class ModernModePlugin {
|
class ModernModePlugin {
|
||||||
constructor({ targetDir, isModernBuild }) {
|
constructor({ targetDir, isModernBuild }) {
|
||||||
this.targetDir = targetDir
|
this.targetDir = targetDir
|
||||||
@ -22,20 +24,33 @@ class ModernModePlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set assets({ name, content }) {
|
||||||
|
assetsMap[name] = content
|
||||||
|
watcher.emit(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssets(name) {
|
||||||
|
return assetsMap[name] ||
|
||||||
|
new Promise((resolve) => {
|
||||||
|
watcher.once(name, () => {
|
||||||
|
return assetsMap[name] && resolve(assetsMap[name])
|
||||||
|
})
|
||||||
|
return assetsMap[name] && resolve(assetsMap[name])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
applyLegacy(compiler) {
|
applyLegacy(compiler) {
|
||||||
const ID = `nuxt-legacy-bundle`
|
const ID = `nuxt-legacy-bundle`
|
||||||
compiler.hooks.compilation.tap(ID, (compilation) => {
|
compiler.hooks.compilation.tap(ID, (compilation) => {
|
||||||
// For html-webpack-plugin 4.0
|
// For html-webpack-plugin 4.0
|
||||||
// HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(ID, async (data, cb) => {
|
// HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(ID, async (data, cb) => {
|
||||||
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(ID, async (data, cb) => {
|
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(ID, (data, cb) => {
|
||||||
// get stats, write to disk
|
// get stats, write to disk
|
||||||
await fs.ensureDir(this.targetDir)
|
this.assets = {
|
||||||
const htmlName = path.basename(data.plugin.options.filename)
|
name: data.plugin.options.filename,
|
||||||
// Watch out for output files in sub directories
|
content: data.body
|
||||||
const htmlPath = path.dirname(data.plugin.options.filename)
|
}
|
||||||
const tempFilename = path.join(this.targetDir, htmlPath, `legacy-assets-${htmlName}.json`)
|
|
||||||
await fs.mkdirp(path.dirname(tempFilename))
|
|
||||||
await fs.writeFile(tempFilename, JSON.stringify(data.body))
|
|
||||||
cb()
|
cb()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -72,15 +87,16 @@ class ModernModePlugin {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// inject links for legacy assets as <script nomodule>
|
// inject links for legacy assets as <script nomodule>
|
||||||
const htmlName = path.basename(data.plugin.options.filename)
|
const fileName = data.plugin.options.filename
|
||||||
// Watch out for output files in sub directories
|
const legacyAssets = (await this.getAssets(fileName))
|
||||||
const htmlPath = path.dirname(data.plugin.options.filename)
|
|
||||||
const tempFilename = path.join(this.targetDir, htmlPath, `legacy-assets-${htmlName}.json`)
|
|
||||||
const legacyAssets = JSON.parse(await fs.readFile(tempFilename, 'utf-8'))
|
|
||||||
.filter(a => a.tagName === 'script' && a.attributes)
|
.filter(a => a.tagName === 'script' && a.attributes)
|
||||||
legacyAssets.forEach(a => (a.attributes.nomodule = ''))
|
|
||||||
data.body.push(...legacyAssets)
|
for (const a of legacyAssets) {
|
||||||
await fs.remove(tempFilename)
|
a.attributes.nomodule = ''
|
||||||
|
data.body.push(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete assetsMap[fileName]
|
||||||
cb()
|
cb()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
2
test/fixtures/modern/nuxt.config.js
vendored
2
test/fixtures/modern/nuxt.config.js
vendored
@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
|
modern: true,
|
||||||
build: {
|
build: {
|
||||||
modern: true,
|
|
||||||
filenames: {
|
filenames: {
|
||||||
app: ({ isModern }) => {
|
app: ({ isModern }) => {
|
||||||
return `${isModern ? 'modern-' : ''}[name].js`
|
return `${isModern ? 'modern-' : ''}[name].js`
|
||||||
|
36
test/unit/modern.client.test.js
Normal file
36
test/unit/modern.client.test.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { loadFixture, getPort, Nuxt, rp } from '../utils'
|
||||||
|
|
||||||
|
let nuxt, port
|
||||||
|
const url = route => 'http://localhost:' + port + route
|
||||||
|
|
||||||
|
describe('modern client mode', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
const options = await loadFixture('modern', { modern: 'client' })
|
||||||
|
nuxt = new Nuxt(options)
|
||||||
|
port = await getPort()
|
||||||
|
await nuxt.server.listen(port, 'localhost')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should contain nomodule legacy resources', async () => {
|
||||||
|
const response = await rp(url('/'))
|
||||||
|
expect(response).toContain('script nomodule src="/_nuxt/app.js')
|
||||||
|
expect(response).toContain('script nomodule src="/_nuxt/commons.app.js')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should contain module modern resources', async () => {
|
||||||
|
const response = await rp(url('/'))
|
||||||
|
expect(response).toContain('<script type="module" src="/_nuxt/modern-app.js"')
|
||||||
|
expect(response).toContain('<script type="module" src="/_nuxt/modern-commons.app.js"')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should contain module preload resources', async () => {
|
||||||
|
const response = await rp(url('/'))
|
||||||
|
expect(response).toContain('<link rel="modulepreload" href="/_nuxt/modern-app.js" as="script">')
|
||||||
|
expect(response).toContain('<link rel="modulepreload" href="/_nuxt/modern-commons.app.js" as="script">')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Close server and ask nuxt to stop listening to file changes
|
||||||
|
afterAll(async () => {
|
||||||
|
await nuxt.close()
|
||||||
|
})
|
||||||
|
})
|
@ -3,9 +3,9 @@ import { loadFixture, getPort, Nuxt, rp } from '../utils'
|
|||||||
let nuxt, port
|
let nuxt, port
|
||||||
const url = route => 'http://localhost:' + port + route
|
const url = route => 'http://localhost:' + port + route
|
||||||
|
|
||||||
describe('modern build', () => {
|
describe('modern server mode', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const options = await loadFixture('modern')
|
const options = await loadFixture('modern', { modern: 'server' })
|
||||||
nuxt = new Nuxt(options)
|
nuxt = new Nuxt(options)
|
||||||
port = await getPort()
|
port = await getPort()
|
||||||
await nuxt.server.listen(port, 'localhost')
|
await nuxt.server.listen(port, 'localhost')
|
||||||
@ -36,4 +36,9 @@ describe('modern build', () => {
|
|||||||
const response = await rp(url('/_nuxt/app.js'))
|
const response = await rp(url('/_nuxt/app.js'))
|
||||||
expect(response).not.toContain('arrow:()=>"build test"')
|
expect(response).not.toContain('arrow:()=>"build test"')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Close server and ask nuxt to stop listening to file changes
|
||||||
|
afterAll(async () => {
|
||||||
|
await nuxt.close()
|
||||||
|
})
|
||||||
})
|
})
|
Loading…
Reference in New Issue
Block a user