feat(vue-renderer): use async fs (#5186)

This commit is contained in:
Pooya Parsa 2019-03-08 15:50:03 +03:30 committed by Xin Du (Clark)
parent 2cbddeb142
commit d07aefa5db
6 changed files with 77 additions and 55 deletions

View File

@ -228,6 +228,9 @@ export default class Server {
} }
async listen(port, host, socket) { async listen(port, host, socket) {
// Don't start listening before nuxt is ready
await this.nuxt.ready()
// Create a new listener // Create a new listener
const listener = new Listener({ const listener = new Listener({
port: isNaN(parseInt(port)) ? this.options.server.port : port, port: isNaN(parseInt(port)) ? this.options.server.port : port,

View File

@ -57,6 +57,7 @@ describe('server: server', () => {
serverMiddleware: [] serverMiddleware: []
}, },
hook: jest.fn(), hook: jest.fn(),
ready: jest.fn(),
callHook: jest.fn(), callHook: jest.fn(),
resolver: { resolver: {
requireModule: jest.fn() requireModule: jest.fn()

View File

@ -1,6 +1,6 @@
import path from 'path' import path from 'path'
import crypto from 'crypto' import crypto from 'crypto'
import fs from 'fs' import fs from 'fs-extra'
import consola from 'consola' import consola from 'consola'
import devalue from '@nuxt/devalue' import devalue from '@nuxt/devalue'
import invert from 'lodash/invert' import invert from 'lodash/invert'
@ -113,7 +113,7 @@ export default class VueRenderer {
async ready() { async ready() {
// -- Development mode -- // -- Development mode --
if (this.context.options.dev) { if (this.context.options.dev) {
this.context.nuxt.hook('build:resources', mfs => this.loadResources(mfs, true)) this.context.nuxt.hook('build:resources', mfs => this.loadResources(mfs))
return return
} }
@ -141,34 +141,28 @@ export default class VueRenderer {
} }
} }
loadResources(_fs, isMFS = false) { async loadResources(_fs) {
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
const readResource = (fileName, encoding) => { const readResource = async (fileName, encoding) => {
try { try {
const fullPath = path.resolve(distPath, fileName) const fullPath = path.resolve(distPath, fileName)
if (!_fs.existsSync(fullPath)) { if (!await _fs.exists(fullPath)) {
return return
} }
const contents = _fs.readFileSync(fullPath, encoding) const contents = await _fs.readFile(fullPath, encoding)
if (isMFS) {
// Cleanup MFS as soon as possible to save memory
_fs.unlinkSync(fullPath)
delete this._assetsMapping
}
return contents return contents
} catch (err) { } catch (err) {
consola.error('Unable to load resource:', fileName, err) consola.error('Unable to load resource:', fileName, err)
} }
} }
for (const resourceName in resourceMap) { for (const resourceName in this.resourceMap) {
const { fileName, transform, encoding } = resourceMap[resourceName] const { fileName, transform, encoding } = this.resourceMap[resourceName]
// Load resource // Load resource
let resource = readResource(fileName, encoding) let resource = await readResource(fileName, encoding)
// Skip unavailable resources // Skip unavailable resources
if (!resource) { if (!resource) {
@ -178,10 +172,7 @@ export default class VueRenderer {
// Apply transforms // Apply transforms
if (typeof transform === 'function') { if (typeof transform === 'function') {
resource = transform(resource, { resource = await transform(resource, { readResource })
readResource,
oldValue: this.context.resources[resourceName]
})
} }
// Update resource // Update resource
@ -189,26 +180,15 @@ export default class VueRenderer {
updated.push(resourceName) updated.push(resourceName)
} }
// Reload error template // Load templates
const errorTemplatePath = path.resolve(this.context.options.buildDir, 'views/error.html') await this.loadTemplates()
if (fs.existsSync(errorTemplatePath)) {
this.context.resources.errorTemplate = this.parseTemplate(
fs.readFileSync(errorTemplatePath, 'utf8')
)
}
// Reload loading template // Detect if any resource updated
const loadingHTMLPath = path.resolve(this.context.options.buildDir, 'loading.html')
if (fs.existsSync(loadingHTMLPath)) {
this.context.resources.loadingHTML = fs.readFileSync(loadingHTMLPath, 'utf8')
this.context.resources.loadingHTML = this.context.resources.loadingHTML
.replace(/\r|\n|[\t\s]{3,}/g, '')
} else {
this.context.resources.loadingHTML = ''
}
// Call createRenderer if any resource changed
if (updated.length > 0) { if (updated.length > 0) {
// Invalidate assetsMapping cache
delete this._assetsMapping
// Create new renderer
this.createRenderer() this.createRenderer()
} }
@ -217,6 +197,24 @@ export default class VueRenderer {
return this.context.nuxt.callHook('render:resourcesLoaded', this.context.resources) return this.context.nuxt.callHook('render:resourcesLoaded', this.context.resources)
} }
async loadTemplates() {
// Reload error template
const errorTemplatePath = path.resolve(this.context.options.buildDir, 'views/error.html')
if (await fs.exists(errorTemplatePath)) {
const errorTemplate = await fs.readFile(errorTemplatePath, 'utf8')
this.context.resources.errorTemplate = this.parseTemplate(errorTemplate)
}
// Reload loading template
const loadingHTMLPath = path.resolve(this.context.options.buildDir, 'loading.html')
if (await fs.exists(loadingHTMLPath)) {
this.context.resources.loadingHTML = await fs.readFile(loadingHTMLPath, 'utf8')
this.context.resources.loadingHTML = this.context.resources.loadingHTML.replace(/\r|\n|[\t\s]{3,}/g, '')
} else {
this.context.resources.loadingHTML = ''
}
}
// TODO: Remove in Nuxt 3 // TODO: Remove in Nuxt 3
get noSSR() { /* Backward compatibility */ get noSSR() { /* Backward compatibility */
return this.context.options.render.ssr === false return this.context.options.render.ssr === false
@ -470,22 +468,21 @@ export default class VueRenderer {
serverManifest: { serverManifest: {
fileName: 'server.manifest.json', fileName: 'server.manifest.json',
// BundleRenderer needs resolved contents // BundleRenderer needs resolved contents
transform: (src, { readResource, oldValue = { files: {}, maps: {} } }) => { transform: async (src, { readResource }) => {
const serverManifest = JSON.parse(src) const serverManifest = JSON.parse(src)
const resolveAssets = (obj, oldObj) => { const readResources = async (obj) => {
Object.keys(obj).forEach((name) => { const _obj = {}
obj[name] = readResource(obj[name]) await Promise.all(Object.keys(obj).map(async (key) => {
// Try to reuse deleted MFS files if no new version exists _obj[key] = await readResource(obj[key])
if (!obj[name]) { }))
obj[name] = oldObj[name] return _obj
}
})
return obj
} }
const files = resolveAssets(serverManifest.files, oldValue.files) const [files, maps] = await Promise.all([
const maps = resolveAssets(serverManifest.maps, oldValue.maps) readResources(serverManifest.files),
readResources(serverManifest.maps)
])
// Try to parse sourcemaps // Try to parse sourcemaps
for (const map in maps) { for (const map in maps) {

View File

@ -1,7 +1,6 @@
import path from 'path' import path from 'path'
import pify from 'pify' import pify from 'pify'
import webpack from 'webpack' import webpack from 'webpack'
import MFS from 'memory-fs'
import Glob from 'glob' import Glob from 'glob'
import webpackDevMiddleware from 'webpack-dev-middleware' import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware' import webpackHotMiddleware from 'webpack-hot-middleware'
@ -12,6 +11,7 @@ import {
sequence, sequence,
wrapArray wrapArray
} from '@nuxt/utils' } from '@nuxt/utils'
import AsyncMFS from './utils/async-mfs'
import { ClientConfig, ModernConfig, ServerConfig } from './config' import { ClientConfig, ModernConfig, ServerConfig } from './config'
import PerfLoader from './utils/perf-loader' import PerfLoader from './utils/perf-loader'
@ -29,11 +29,7 @@ export class WebpackBundler {
// Initialize shared MFS for dev // Initialize shared MFS for dev
if (this.buildContext.options.dev) { if (this.buildContext.options.dev) {
this.mfs = new MFS() this.mfs = new AsyncMFS()
// TODO: Enable when async FS required
// this.mfs.exists = function (...args) { return Promise.resolve(this.existsSync(...args)) }
// this.mfs.readFile = function (...args) { return Promise.resolve(this.readFileSync(...args)) }
} }
} }

View File

@ -0,0 +1,24 @@
import MFS from 'memory-fs'
export default class AsyncMFS extends MFS {}
const syncRegex = /Sync$/
const propsToPromisify = Object.getOwnPropertyNames(MFS.prototype).filter(n => syncRegex.test(n))
for (const prop of propsToPromisify) {
const asyncProp = prop.replace(syncRegex, '')
const origAsync = AsyncMFS.prototype[asyncProp]
AsyncMFS.prototype[asyncProp] = function (...args) {
// Callback support for webpack
if (origAsync && args.length && typeof args[args.length - 1] === 'function') {
return origAsync.call(this, ...args)
}
try {
return Promise.resolve(MFS.prototype[prop].call(this, ...args))
} catch (error) {
return Promise.reject(error)
}
}
}

View File

@ -13,6 +13,7 @@ describe('express', () => {
beforeAll(async () => { beforeAll(async () => {
const config = await loadFixture('basic') const config = await loadFixture('basic')
nuxt = new Nuxt(config) nuxt = new Nuxt(config)
await nuxt.ready()
port = await getPort() port = await getPort()