mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-23 14:15:13 +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) {
|
||||
const { Nuxt } = await imports.core()
|
||||
return new Nuxt(options)
|
||||
const nuxt = new Nuxt(options)
|
||||
await nuxt.ready()
|
||||
return 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 { showBanner } from '../utils'
|
||||
|
||||
@ -17,32 +14,10 @@ export default {
|
||||
|
||||
// Create production build when calling `nuxt build`
|
||||
const nuxt = await cmd.getNuxt(
|
||||
await cmd.getNuxtConfig(argv, { dev: false })
|
||||
await cmd.getNuxtConfig(argv, { dev: false, _start: true })
|
||||
)
|
||||
|
||||
// Check if project is built for production
|
||||
const distDir = path.resolve(
|
||||
nuxt.options.rootDir,
|
||||
nuxt.options.buildDir || '.nuxt',
|
||||
'dist',
|
||||
'server'
|
||||
)
|
||||
if (!fs.existsSync(distDir)) {
|
||||
consola.fatal(
|
||||
'No build files found, please run `nuxt build` before launching `nuxt start`'
|
||||
)
|
||||
}
|
||||
|
||||
// Check if SSR Bundle is required
|
||||
if (nuxt.options.render.ssr === true) {
|
||||
const ssrBundlePath = path.resolve(distDir, 'server-bundle.json')
|
||||
if (!fs.existsSync(ssrBundlePath)) {
|
||||
consola.fatal(
|
||||
'No SSR build found.\nPlease start with `nuxt start --spa` or build using `nuxt build --universal`'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen and show ready banner
|
||||
return nuxt.server.listen().then(() => {
|
||||
showBanner(nuxt)
|
||||
})
|
||||
|
@ -1,4 +1,4 @@
|
||||
import fs from 'fs'
|
||||
import fs from 'fs-extra'
|
||||
import { consola, mockGetNuxtStart, mockGetNuxtConfig, NuxtCommand } from '../utils'
|
||||
|
||||
describe('start', () => {
|
||||
@ -22,41 +22,14 @@ describe('start', () => {
|
||||
test('no error if dist dir exists', async () => {
|
||||
mockGetNuxtStart()
|
||||
mockGetNuxtConfig()
|
||||
jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => true)
|
||||
|
||||
await NuxtCommand.from(start).run()
|
||||
|
||||
expect(consola.fatal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('fatal error if dist dir doesnt exist', async () => {
|
||||
mockGetNuxtStart()
|
||||
jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => false)
|
||||
|
||||
await NuxtCommand.from(start).run()
|
||||
|
||||
expect(consola.fatal).toHaveBeenCalledWith('No build files found, please run `nuxt build` before launching `nuxt start`')
|
||||
})
|
||||
|
||||
test('no error on ssr and server bundle exists', async () => {
|
||||
mockGetNuxtStart(true)
|
||||
mockGetNuxtConfig()
|
||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => true)
|
||||
|
||||
await NuxtCommand.from(start).run()
|
||||
|
||||
expect(consola.fatal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test.skip('fatal error on ssr and server bundle doesnt exist', async () => {
|
||||
mockGetNuxtStart(true)
|
||||
let i = 0
|
||||
jest.spyOn(fs, 'existsSync').mockImplementation(() => {
|
||||
return ++i === 1
|
||||
})
|
||||
|
||||
await NuxtCommand.from(start).run()
|
||||
|
||||
expect(consola.fatal).toHaveBeenCalledWith('No SSR build found.\nPlease start with `nuxt start --spa` or build using `nuxt build --universal`')
|
||||
})
|
||||
})
|
||||
|
@ -91,6 +91,7 @@ export const mockNuxt = (implementation) => {
|
||||
options: {},
|
||||
clearHook: jest.fn(),
|
||||
close: jest.fn(),
|
||||
ready: jest.fn(),
|
||||
server: {
|
||||
listeners: [],
|
||||
listen: jest.fn().mockImplementationOnce(() => Promise.resolve())
|
||||
|
@ -64,8 +64,7 @@ export default ({ resources, options }) => function errorMiddleware(err, req, re
|
||||
readSourceFactory({
|
||||
srcDir: options.srcDir,
|
||||
rootDir: options.rootDir,
|
||||
buildDir: options.buildDir,
|
||||
resources
|
||||
buildDir: options.buildDir
|
||||
}),
|
||||
options.router.base,
|
||||
true
|
||||
@ -79,7 +78,7 @@ export default ({ resources, options }) => function errorMiddleware(err, req, re
|
||||
}
|
||||
}
|
||||
|
||||
const readSourceFactory = ({ srcDir, rootDir, buildDir, resources }) => async function readSource(frame) {
|
||||
const readSourceFactory = ({ srcDir, rootDir, buildDir }) => async function readSource(frame) {
|
||||
// Remove webpack:/// & query string from the end
|
||||
const sanitizeName = name =>
|
||||
name ? name.replace('webpack:///', '').split('?')[0] : null
|
||||
@ -113,11 +112,4 @@ const readSourceFactory = ({ srcDir, rootDir, buildDir, resources }) => async fu
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use server bundle
|
||||
// TODO: restore to if after https://github.com/istanbuljs/nyc/issues/595 fixed
|
||||
/* istanbul ignore next */
|
||||
if (!frame.contents) {
|
||||
frame.contents = resources.serverBundle.files[frame.fileName]
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,14 @@
|
||||
],
|
||||
"main": "dist/vue-app.js",
|
||||
"typings": "types/index.d.ts",
|
||||
"dependencies": {
|
||||
"vue": "^2.5.17",
|
||||
"vue-meta": "^1.5.6",
|
||||
"vue-no-ssr": "^1.1.0",
|
||||
"vue-router": "^3.0.2",
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
|
@ -15,11 +15,7 @@
|
||||
"lru-cache": "^5.1.1",
|
||||
"vue": "^2.5.17",
|
||||
"vue-meta": "^1.5.6",
|
||||
"vue-no-ssr": "^1.1.0",
|
||||
"vue-router": "^3.0.2",
|
||||
"vue-server-renderer": "^2.5.17",
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
"vuex": "^3.0.1"
|
||||
"vue-server-renderer": "^2.5.17"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import path from 'path'
|
||||
import crypto from 'crypto'
|
||||
import fs from 'fs-extra'
|
||||
import fs from 'fs'
|
||||
import consola from 'consola'
|
||||
import devalue from '@nuxtjs/devalue'
|
||||
import invert from 'lodash/invert'
|
||||
@ -16,19 +16,19 @@ export default class VueRenderer {
|
||||
|
||||
// Will be set by createRenderer
|
||||
this.renderer = {
|
||||
ssr: null,
|
||||
modern: null,
|
||||
spa: null
|
||||
ssr: undefined,
|
||||
modern: undefined,
|
||||
spa: undefined
|
||||
}
|
||||
|
||||
// Renderer runtime resources
|
||||
Object.assign(this.context.resources, {
|
||||
clientManifest: null,
|
||||
modernManifest: null,
|
||||
serverBundle: null,
|
||||
ssrTemplate: null,
|
||||
spaTemplate: null,
|
||||
errorTemplate: this.constructor.parseTemplate('Nuxt.js Internal Server Error')
|
||||
clientManifest: undefined,
|
||||
modernManifest: undefined,
|
||||
serverManifest: undefined,
|
||||
ssrTemplate: undefined,
|
||||
spaTemplate: undefined,
|
||||
errorTemplate: this.parseTemplate('Nuxt.js Internal Server Error')
|
||||
})
|
||||
}
|
||||
|
||||
@ -98,57 +98,78 @@ export default class VueRenderer {
|
||||
}
|
||||
|
||||
async ready() {
|
||||
// Production: Load SSR resources from fs
|
||||
if (!this.context.options.dev) {
|
||||
await this.loadResources()
|
||||
// Production: Load SSR resources from fs
|
||||
await this.loadResources(fs)
|
||||
|
||||
// Verify
|
||||
if (!this.isReady && this.context.options._start) {
|
||||
throw new Error(
|
||||
'No build files found. Use either `nuxt build` or `builder.build()` or start nuxt in development mode.'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Development: Listen on build:resources hook
|
||||
this.context.nuxt.hook('build:resources', mfs => this.loadResources(mfs, true))
|
||||
}
|
||||
}
|
||||
|
||||
async loadResources(_fs = fs) {
|
||||
loadResources(_fs, isMFS = false) {
|
||||
const distPath = path.resolve(this.context.options.buildDir, 'dist', 'server')
|
||||
const updated = []
|
||||
const resourceMap = this.resourceMap
|
||||
|
||||
this.constructor.resourceMap.forEach(({ key, fileName, transform }) => {
|
||||
const rawKey = '$$' + key
|
||||
const _path = path.join(distPath, fileName)
|
||||
const readResource = (fileName, encoding) => {
|
||||
try {
|
||||
const fullPath = path.resolve(distPath, fileName)
|
||||
if (!_fs.existsSync(fullPath)) {
|
||||
return
|
||||
}
|
||||
const contents = _fs.readFileSync(fullPath, encoding)
|
||||
if (isMFS) {
|
||||
// Cleanup MFS as soon as possible to save memory
|
||||
_fs.unlinkSync(fullPath)
|
||||
}
|
||||
return contents
|
||||
} catch (err) {
|
||||
consola.error('Unable to load resource:', fileName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Fail when no build found and using programmatic usage
|
||||
if (!_fs.existsSync(_path)) {
|
||||
// TODO: Enable baack when renderer initialzation was disabled for build only scripts
|
||||
// Currently this breaks normal nuxt build for first time
|
||||
// if (!this.context.options.dev) {
|
||||
// const invalidSSR = !this.noSSR && key === 'serverBundle'
|
||||
// const invalidSPA = this.noSSR && key === 'spaTemplate'
|
||||
// if (invalidSPA || invalidSSR) {
|
||||
// consola.fatal(`Could not load Nuxt renderer, make sure to build for production: builder.build() with dev option set to false.`)
|
||||
// }
|
||||
// }
|
||||
return // Resource not exists
|
||||
for (const resourceName in resourceMap) {
|
||||
const { fileName, transform, encoding } = resourceMap[resourceName]
|
||||
|
||||
// Load resource
|
||||
let resource = readResource(fileName, encoding)
|
||||
|
||||
// Skip unavailable resources
|
||||
if (!resource) {
|
||||
consola.debug('Resource not available:', resourceName)
|
||||
continue
|
||||
}
|
||||
|
||||
const rawData = _fs.readFileSync(_path, 'utf8')
|
||||
if (!rawData || rawData === this.context.resources[rawKey]) {
|
||||
return // No changes
|
||||
// Apply transforms
|
||||
if (typeof transform === 'function') {
|
||||
resource = transform(resource, {
|
||||
readResource,
|
||||
oldValue: this.context.resources[resourceName]
|
||||
})
|
||||
}
|
||||
this.context.resources[rawKey] = rawData
|
||||
const data = transform(rawData)
|
||||
/* istanbul ignore if */
|
||||
if (!data) {
|
||||
return // Invalid data ?
|
||||
}
|
||||
this.context.resources[key] = data
|
||||
updated.push(key)
|
||||
})
|
||||
|
||||
// Update resource
|
||||
this.context.resources[resourceName] = resource
|
||||
updated.push(resourceName)
|
||||
}
|
||||
|
||||
// Reload error template
|
||||
const errorTemplatePath = path.resolve(this.context.options.buildDir, 'views/error.html')
|
||||
if (fs.existsSync(errorTemplatePath)) {
|
||||
this.context.resources.errorTemplate = this.constructor.parseTemplate(
|
||||
this.context.resources.errorTemplate = this.parseTemplate(
|
||||
fs.readFileSync(errorTemplatePath, 'utf8')
|
||||
)
|
||||
}
|
||||
|
||||
// Load loading template
|
||||
// Reload loading template
|
||||
const loadingHTMLPath = path.resolve(this.context.options.buildDir, 'loading.html')
|
||||
if (fs.existsSync(loadingHTMLPath)) {
|
||||
this.context.resources.loadingHTML = fs.readFileSync(loadingHTMLPath, 'utf8')
|
||||
@ -158,57 +179,60 @@ export default class VueRenderer {
|
||||
this.context.resources.loadingHTML = ''
|
||||
}
|
||||
|
||||
// Call resourcesLoaded plugin
|
||||
await this.context.nuxt.callHook('render:resourcesLoaded', this.context.resources)
|
||||
|
||||
// Call createRenderer if any resource changed
|
||||
if (updated.length > 0) {
|
||||
this.createRenderer()
|
||||
}
|
||||
|
||||
// Call resourcesLoaded hook
|
||||
consola.debug('Resources loaded:', updated.join(','))
|
||||
return this.context.nuxt.callHook('render:resourcesLoaded', this.context.resources)
|
||||
}
|
||||
|
||||
get noSSR() {
|
||||
get noSSR() { /* Backward compatibility */
|
||||
return this.context.options.render.ssr === false
|
||||
}
|
||||
|
||||
get isReady() {
|
||||
if (this.noSSR) {
|
||||
return Boolean(this.context.resources.spaTemplate)
|
||||
}
|
||||
|
||||
return Boolean(this.renderer.ssr && this.context.resources.ssrTemplate)
|
||||
get SSR() {
|
||||
return this.context.options.render.ssr === true
|
||||
}
|
||||
|
||||
get isResourcesAvailable() {
|
||||
// Required for both
|
||||
/* istanbul ignore if */
|
||||
if (!this.context.resources.clientManifest) {
|
||||
get isReady() {
|
||||
// SPA
|
||||
if (!this.context.resources.spaTemplate || !this.renderer.spa) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Required for SPA rendering
|
||||
if (this.noSSR) {
|
||||
return Boolean(this.context.resources.spaTemplate)
|
||||
// SSR
|
||||
if (this.SSR && (!this.context.resources.ssrTemplate || !this.renderer.ssr)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Required for bundle renderer
|
||||
return Boolean(this.context.resources.ssrTemplate && this.context.resources.serverBundle)
|
||||
return true
|
||||
}
|
||||
|
||||
get isResourcesAvailable() { /* Backward compatibility */
|
||||
return this.isReady
|
||||
}
|
||||
|
||||
createRenderer() {
|
||||
// Ensure resources are available
|
||||
if (!this.isResourcesAvailable) {
|
||||
// Resource clientManifest is always required
|
||||
if (!this.context.resources.clientManifest) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create Meta Renderer
|
||||
this.renderer.spa = new SPAMetaRenderer(this)
|
||||
// Create SPA renderer
|
||||
if (this.context.resources.spaTemplate) {
|
||||
this.renderer.spa = new SPAMetaRenderer(this)
|
||||
}
|
||||
|
||||
// Skip following steps if noSSR mode
|
||||
if (this.noSSR) {
|
||||
// Skip the rest if SSR resources are not available
|
||||
if (!this.context.resources.ssrTemplate || !this.context.resources.serverManifest) {
|
||||
return
|
||||
}
|
||||
|
||||
const hasModules = fs.existsSync(path.resolve(this.context.options.rootDir, 'node_modules'))
|
||||
|
||||
const rendererOptions = {
|
||||
runInNewContext: false,
|
||||
clientManifest: this.context.resources.clientManifest,
|
||||
@ -219,14 +243,14 @@ export default class VueRenderer {
|
||||
|
||||
// Create bundle renderer for SSR
|
||||
this.renderer.ssr = createBundleRenderer(
|
||||
this.context.resources.serverBundle,
|
||||
this.context.resources.serverManifest,
|
||||
rendererOptions
|
||||
)
|
||||
|
||||
if (this.context.resources.modernManifest &&
|
||||
!['client', false].includes(this.context.options.modern)) {
|
||||
this.renderer.modern = createBundleRenderer(
|
||||
this.context.resources.serverBundle,
|
||||
this.context.resources.serverManifest,
|
||||
{
|
||||
...rendererOptions,
|
||||
clientManifest: this.context.resources.modernManifest
|
||||
@ -248,6 +272,7 @@ export default class VueRenderer {
|
||||
async renderRoute(url, context = {}) {
|
||||
/* istanbul ignore if */
|
||||
if (!this.isReady) {
|
||||
consola.info('Waiting for server resources...')
|
||||
await waitFor(1000)
|
||||
return this.renderRoute(url, context)
|
||||
}
|
||||
@ -258,12 +283,12 @@ export default class VueRenderer {
|
||||
// Add url and isSever to the context
|
||||
context.url = url
|
||||
|
||||
// Basic response if SSR is disabled or spa data provided
|
||||
// Basic response if SSR is disabled or SPA data provided
|
||||
const { req, res } = context
|
||||
const spa = context.spa || (res && res.spa)
|
||||
const ENV = this.context.options.env
|
||||
|
||||
if (this.noSSR || spa) {
|
||||
if (!this.SSR || spa) {
|
||||
const {
|
||||
HTML_ATTRS,
|
||||
BODY_ATTRS,
|
||||
@ -348,39 +373,57 @@ export default class VueRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
static parseTemplate(templateStr) {
|
||||
get resourceMap() {
|
||||
return {
|
||||
clientManifest: {
|
||||
fileName: 'client.manifest.json',
|
||||
transform: src => JSON.parse(src)
|
||||
},
|
||||
modernManifest: {
|
||||
fileName: 'modern.manifest.json',
|
||||
transform: src => JSON.parse(src)
|
||||
},
|
||||
serverManifest: {
|
||||
fileName: 'server.manifest.json',
|
||||
// BundleRenderer needs resolved contents
|
||||
transform: (src, { readResource, oldValue = { files: {}, maps: {} } }) => {
|
||||
const serverManifest = JSON.parse(src)
|
||||
|
||||
const resolveAssets = (obj, oldObj, encoding) => {
|
||||
Object.keys(obj).forEach((name) => {
|
||||
obj[name] = readResource(obj[name], encoding)
|
||||
// Try to reuse deleted MFS files if no new version exists
|
||||
if (!obj[name]) {
|
||||
obj[name] = oldObj[name]
|
||||
}
|
||||
})
|
||||
return obj
|
||||
}
|
||||
|
||||
const files = resolveAssets(serverManifest.files, oldValue.files)
|
||||
const maps = resolveAssets(serverManifest.maps, oldValue.maps, 'utf-8')
|
||||
|
||||
return {
|
||||
...serverManifest,
|
||||
files,
|
||||
maps
|
||||
}
|
||||
}
|
||||
},
|
||||
ssrTemplate: {
|
||||
fileName: 'index.ssr.html',
|
||||
transform: src => this.parseTemplate(src)
|
||||
},
|
||||
spaTemplate: {
|
||||
fileName: 'index.spa.html',
|
||||
transform: src => this.parseTemplate(src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseTemplate(templateStr) {
|
||||
return template(templateStr, {
|
||||
interpolate: /{{([\s\S]+?)}}/g
|
||||
})
|
||||
}
|
||||
|
||||
static get resourceMap() {
|
||||
return [
|
||||
{
|
||||
key: 'clientManifest',
|
||||
fileName: 'vue-ssr-client-manifest.json',
|
||||
transform: JSON.parse
|
||||
},
|
||||
{
|
||||
key: 'modernManifest',
|
||||
fileName: 'vue-ssr-modern-manifest.json',
|
||||
transform: JSON.parse
|
||||
},
|
||||
{
|
||||
key: 'serverBundle',
|
||||
fileName: 'server-bundle.json',
|
||||
transform: JSON.parse
|
||||
},
|
||||
{
|
||||
key: 'ssrTemplate',
|
||||
fileName: 'index.ssr.html',
|
||||
transform: this.parseTemplate
|
||||
},
|
||||
{
|
||||
key: 'spaTemplate',
|
||||
fileName: 'index.spa.html',
|
||||
transform: this.parseTemplate
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import pify from 'pify'
|
||||
import webpack from 'webpack'
|
||||
@ -14,7 +13,8 @@ import {
|
||||
wrapArray
|
||||
} from '@nuxt/common'
|
||||
|
||||
import { ClientConfig, ModernConfig, ServerConfig, PerfLoader } from './config'
|
||||
import { ClientConfig, ModernConfig, ServerConfig } from './config'
|
||||
import PerfLoader from './utils/perf-loader'
|
||||
|
||||
const glob = pify(Glob)
|
||||
|
||||
@ -27,9 +27,11 @@ export class WebpackBundler {
|
||||
this.devMiddleware = {}
|
||||
this.hotMiddleware = {}
|
||||
|
||||
// Initialize shared FS and Cache
|
||||
// Initialize shared MFS for dev
|
||||
if (this.context.options.dev) {
|
||||
this.mfs = new MFS()
|
||||
this.mfs.exists = function (...args) { return Promise.resolve(this.existsSync(...args)) }
|
||||
this.mfs.readFile = function (...args) { return Promise.resolve(this.readFileSync(...args)) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +84,7 @@ export class WebpackBundler {
|
||||
'Please use https://github.com/nuxt-community/style-resources-module'
|
||||
)
|
||||
}
|
||||
Object.keys(styleResources).forEach(async (ext) => {
|
||||
for (const ext of Object.keys(styleResources)) {
|
||||
await Promise.all(wrapArray(styleResources[ext]).map(async (p) => {
|
||||
const styleResourceFiles = await glob(path.resolve(this.context.options.rootDir, p))
|
||||
|
||||
@ -90,7 +92,7 @@ export class WebpackBundler {
|
||||
throw new Error(`Style Resource not found: ${p}`)
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
// Configure compilers
|
||||
this.compilers = compilersOptions.map((compilersOption) => {
|
||||
@ -135,7 +137,7 @@ export class WebpackBundler {
|
||||
})
|
||||
|
||||
// Reload renderer if available
|
||||
nuxt.server.loadResources(this.mfs || fs)
|
||||
await nuxt.callHook('build:resources', this.mfs)
|
||||
|
||||
// Resolve on next tick
|
||||
process.nextTick(resolve)
|
||||
|
@ -6,14 +6,15 @@ import cloneDeep from 'lodash/cloneDeep'
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
import VueLoader from 'vue-loader'
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
|
||||
import TerserWebpackPlugin from 'terser-webpack-plugin'
|
||||
import WebpackBar from 'webpackbar'
|
||||
import env from 'std-env'
|
||||
|
||||
import { isUrl, urlJoin } from '@nuxt/common'
|
||||
|
||||
import PerfLoader from './utils/perf-loader'
|
||||
import StyleLoader from './utils/style-loader'
|
||||
import WarnFixPlugin from './plugins/warnfix'
|
||||
import PerfLoader from '../utils/perf-loader'
|
||||
import StyleLoader from '../utils/style-loader'
|
||||
import WarnFixPlugin from '../plugins/warnfix'
|
||||
|
||||
export default class WebpackBaseConfig {
|
||||
constructor(builder, options) {
|
||||
@ -96,7 +97,7 @@ export default class WebpackBaseConfig {
|
||||
return fileName
|
||||
}
|
||||
|
||||
devtool() {
|
||||
get devtool() {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -127,7 +128,41 @@ export default class WebpackBaseConfig {
|
||||
}
|
||||
|
||||
optimization() {
|
||||
return this.options.build.optimization
|
||||
const optimization = cloneDeep(this.options.build.optimization)
|
||||
|
||||
if (optimization.minimize && optimization.minimizer === undefined) {
|
||||
optimization.minimizer = this.minimizer()
|
||||
}
|
||||
|
||||
return optimization
|
||||
}
|
||||
|
||||
minimizer() {
|
||||
const minimizer = []
|
||||
|
||||
// https://github.com/webpack-contrib/terser-webpack-plugin
|
||||
if (this.options.build.terser) {
|
||||
minimizer.push(
|
||||
new TerserWebpackPlugin(Object.assign({
|
||||
parallel: true,
|
||||
cache: this.options.build.cache,
|
||||
sourceMap: this.devtool && /source-?map/.test(this.devtool),
|
||||
extractComments: {
|
||||
filename: 'LICENSES'
|
||||
},
|
||||
terserOptions: {
|
||||
compress: {
|
||||
ecma: this.isModern ? 6 : undefined
|
||||
},
|
||||
output: {
|
||||
comments: /^\**!|@preserve|@license|@cc_on/
|
||||
}
|
||||
}
|
||||
}, this.options.build.terser))
|
||||
)
|
||||
}
|
||||
|
||||
return minimizer
|
||||
}
|
||||
|
||||
alias() {
|
||||
@ -337,10 +372,11 @@ export default class WebpackBaseConfig {
|
||||
config() {
|
||||
// Prioritize nested node_modules in webpack search path (#2558)
|
||||
const webpackModulesDir = ['node_modules'].concat(this.options.modulesDir)
|
||||
|
||||
const config = {
|
||||
name: this.name,
|
||||
mode: this.buildMode,
|
||||
devtool: this.devtool(),
|
||||
devtool: this.devtool,
|
||||
optimization: this.optimization(),
|
||||
output: this.output(),
|
||||
performance: {
|
||||
|
@ -2,12 +2,11 @@ import path from 'path'
|
||||
import webpack from 'webpack'
|
||||
import HTMLPlugin from 'html-webpack-plugin'
|
||||
import BundleAnalyzer from 'webpack-bundle-analyzer'
|
||||
import TerserWebpackPlugin from 'terser-webpack-plugin'
|
||||
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'
|
||||
import FriendlyErrorsWebpackPlugin from '@nuxt/friendly-errors-webpack-plugin'
|
||||
|
||||
import ModernModePlugin from './plugins/vue/modern'
|
||||
import VueSSRClientPlugin from './plugins/vue/client'
|
||||
import ModernModePlugin from '../plugins/vue/modern'
|
||||
import VueSSRClientPlugin from '../plugins/vue/client'
|
||||
import WebpackBaseConfig from './base'
|
||||
|
||||
export default class WebpackClientConfig extends WebpackBaseConfig {
|
||||
@ -54,6 +53,21 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
|
||||
return optimization
|
||||
}
|
||||
|
||||
minimizer() {
|
||||
const minimizer = super.minimizer()
|
||||
|
||||
// https://github.com/NMFR/optimize-css-assets-webpack-plugin
|
||||
// https://github.com/webpack-contrib/mini-css-extract-plugin#minimizing-for-production
|
||||
// TODO: Remove OptimizeCSSAssetsPlugin when upgrading to webpack 5
|
||||
if (this.options.build.optimizeCSS) {
|
||||
minimizer.push(
|
||||
new OptimizeCSSAssetsPlugin(Object.assign({}, this.options.build.optimizeCSS))
|
||||
)
|
||||
}
|
||||
|
||||
return minimizer
|
||||
}
|
||||
|
||||
plugins() {
|
||||
const plugins = super.plugins()
|
||||
|
||||
@ -78,7 +92,7 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
|
||||
chunksSortMode: 'dependency'
|
||||
}),
|
||||
new VueSSRClientPlugin({
|
||||
filename: `../server/vue-ssr-${this.name}-manifest.json`
|
||||
filename: `../server/${this.name}.manifest.json`
|
||||
}),
|
||||
new webpack.DefinePlugin(this.env())
|
||||
)
|
||||
@ -113,48 +127,6 @@ export default class WebpackClientConfig extends WebpackBaseConfig {
|
||||
return plugins
|
||||
}
|
||||
|
||||
extendConfig() {
|
||||
const config = super.extendConfig(...arguments)
|
||||
|
||||
// Add minimizer plugins
|
||||
if (config.optimization.minimize && config.optimization.minimizer === undefined) {
|
||||
config.optimization.minimizer = []
|
||||
|
||||
// https://github.com/webpack-contrib/terser-webpack-plugin
|
||||
if (this.options.build.terser) {
|
||||
config.optimization.minimizer.push(
|
||||
new TerserWebpackPlugin(Object.assign({
|
||||
parallel: true,
|
||||
cache: this.options.build.cache,
|
||||
sourceMap: config.devtool && /source-?map/.test(config.devtool),
|
||||
extractComments: {
|
||||
filename: 'LICENSES'
|
||||
},
|
||||
terserOptions: {
|
||||
compress: {
|
||||
ecma: this.isModern ? 6 : undefined
|
||||
},
|
||||
output: {
|
||||
comments: /^\**!|@preserve|@license|@cc_on/
|
||||
}
|
||||
}
|
||||
}, this.options.build.terser))
|
||||
)
|
||||
}
|
||||
|
||||
// https://github.com/NMFR/optimize-css-assets-webpack-plugin
|
||||
// https://github.com/webpack-contrib/mini-css-extract-plugin#minimizing-for-production
|
||||
// TODO: Remove OptimizeCSSAssetsPlugin when upgrading to webpack 5
|
||||
if (this.options.build.optimizeCSS) {
|
||||
config.optimization.minimizer.push(
|
||||
new OptimizeCSSAssetsPlugin(Object.assign({}, this.options.build.optimizeCSS))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
config() {
|
||||
const config = super.config()
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
export { default as ClientConfig } from './client'
|
||||
export { default as ModernConfig } from './modern'
|
||||
export { default as ServerConfig } from './server'
|
||||
export { default as PerfLoader } from './utils/perf-loader'
|
||||
|
@ -4,8 +4,9 @@ import webpack from 'webpack'
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
import nodeExternals from 'webpack-node-externals'
|
||||
|
||||
import VueSSRServerPlugin from '../plugins/vue/server'
|
||||
|
||||
import WebpackBaseConfig from './base'
|
||||
import VueSSRServerPlugin from './plugins/vue/server'
|
||||
|
||||
export default class WebpackServerConfig extends WebpackBaseConfig {
|
||||
constructor(builder) {
|
||||
@ -29,8 +30,8 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
|
||||
return whitelist
|
||||
}
|
||||
|
||||
devtool() {
|
||||
return 'cheap-module-inline-source-map'
|
||||
get devtool() {
|
||||
return 'cheap-module-source-map'
|
||||
}
|
||||
|
||||
env() {
|
||||
@ -45,7 +46,7 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
|
||||
optimization() {
|
||||
return {
|
||||
splitChunks: false,
|
||||
minimizer: []
|
||||
minimizer: this.minimizer()
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,7 +54,7 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
|
||||
const plugins = super.plugins()
|
||||
plugins.push(
|
||||
new VueSSRServerPlugin({
|
||||
filename: 'server-bundle.json'
|
||||
filename: `${this.name}.manifest.json`
|
||||
}),
|
||||
new webpack.DefinePlugin(this.env())
|
||||
)
|
||||
@ -70,11 +71,12 @@ export default class WebpackServerConfig extends WebpackBaseConfig {
|
||||
app: [path.resolve(this.options.buildDir, 'server.js')]
|
||||
},
|
||||
output: Object.assign({}, config.output, {
|
||||
filename: 'server-bundle.js',
|
||||
filename: 'server.js',
|
||||
libraryTarget: 'commonjs2'
|
||||
}),
|
||||
performance: {
|
||||
hints: false,
|
||||
maxEntrypointSize: Infinity,
|
||||
maxAssetSize: Infinity
|
||||
},
|
||||
externals: []
|
||||
|
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* This file is based on Vue.js (MIT) webpack plugins
|
||||
* https://github.com/vuejs/vue/blob/dev/src/server/webpack-plugin/client.js
|
||||
*/
|
||||
|
||||
import hash from 'hash-sum'
|
||||
import uniq from 'lodash/uniq'
|
||||
|
||||
@ -6,7 +11,7 @@ import { isJS, isCSS, onEmit } from './util'
|
||||
export default class VueSSRClientPlugin {
|
||||
constructor(options = {}) {
|
||||
this.options = Object.assign({
|
||||
filename: 'vue-ssr-client-manifest.json'
|
||||
filename: null
|
||||
}, options)
|
||||
}
|
||||
|
||||
@ -45,7 +50,7 @@ export default class VueSSRClientPlugin {
|
||||
const assetModules = stats.modules.filter(m => m.assets.length)
|
||||
const fileToIndex = file => manifest.all.indexOf(file)
|
||||
stats.modules.forEach((m) => {
|
||||
// ignore modules duplicated in multiple chunks
|
||||
// Ignore modules duplicated in multiple chunks
|
||||
if (m.chunks.length === 1) {
|
||||
const cid = m.chunks[0]
|
||||
const chunk = stats.chunks.find(c => c.id === cid)
|
||||
@ -54,7 +59,8 @@ export default class VueSSRClientPlugin {
|
||||
}
|
||||
const id = m.identifier.replace(/\s\w+$/, '') // remove appended hash
|
||||
const files = manifest.modules[hash(id)] = chunk.files.map(fileToIndex)
|
||||
// find all asset modules associated with the same chunk
|
||||
|
||||
// Find all asset modules associated with the same chunk
|
||||
assetModules.forEach((m) => {
|
||||
if (m.chunks.some(id => id === cid)) {
|
||||
files.push.apply(files, m.assets.map(fileToIndex))
|
||||
@ -63,16 +69,11 @@ export default class VueSSRClientPlugin {
|
||||
}
|
||||
})
|
||||
|
||||
// const debug = (file, obj) => {
|
||||
// require('fs').writeFileSync(__dirname + '/' + file, JSON.stringify(obj, null, 2))
|
||||
// }
|
||||
// debug('stats.json', stats)
|
||||
// debug('client-manifest.json', manifest)
|
||||
const src = JSON.stringify(manifest, null, 2)
|
||||
|
||||
const json = JSON.stringify(manifest, null, 2)
|
||||
compilation.assets[this.options.filename] = {
|
||||
source: () => json,
|
||||
size: () => json.length
|
||||
source: () => src,
|
||||
size: () => src.length
|
||||
}
|
||||
cb()
|
||||
})
|
@ -1,16 +1,14 @@
|
||||
/*
|
||||
** This plugin is inspired by @vue/cli-service ModernModePlugin
|
||||
** https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js
|
||||
* This file is based on @vue/cli-service (MIT) ModernModePlugin
|
||||
* https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/webpack/ModernModePlugin.js
|
||||
*/
|
||||
import EventEmitter from 'events'
|
||||
|
||||
// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
|
||||
const safariFix = `!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();`
|
||||
import EventEmitter from 'events'
|
||||
|
||||
const assetsMap = {}
|
||||
const watcher = new EventEmitter()
|
||||
|
||||
class ModernModePlugin {
|
||||
export default class ModernModePlugin {
|
||||
constructor({ targetDir, isModernBuild }) {
|
||||
this.targetDir = targetDir
|
||||
this.isModernBuild = isModernBuild
|
||||
@ -83,7 +81,7 @@ class ModernModePlugin {
|
||||
data.body.push({
|
||||
tagName: 'script',
|
||||
closeTag: true,
|
||||
innerHTML: safariFix
|
||||
innerHTML: ModernModePlugin.safariFix
|
||||
})
|
||||
|
||||
// inject links for legacy assets as <script nomodule>
|
||||
@ -107,6 +105,5 @@ class ModernModePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
ModernModePlugin.safariFix = safariFix
|
||||
|
||||
export default ModernModePlugin
|
||||
// https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
|
||||
ModernModePlugin.safariFix = `!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();`
|
@ -3,7 +3,7 @@ import { validate, isJS, onEmit } from './util'
|
||||
export default class VueSSRServerPlugin {
|
||||
constructor(options = {}) {
|
||||
this.options = Object.assign({
|
||||
filename: 'vue-ssr-server-bundle.json'
|
||||
filename: null
|
||||
}, options)
|
||||
}
|
||||
|
||||
@ -44,20 +44,17 @@ export default class VueSSRServerPlugin {
|
||||
|
||||
stats.assets.forEach((asset) => {
|
||||
if (isJS(asset.name)) {
|
||||
bundle.files[asset.name] = compilation.assets[asset.name].source()
|
||||
bundle.files[asset.name] = asset.name
|
||||
} else if (asset.name.match(/\.js\.map$/)) {
|
||||
bundle.maps[asset.name.replace(/\.map$/, '')] = JSON.parse(compilation.assets[asset.name].source())
|
||||
bundle.maps[asset.name.replace(/\.map$/, '')] = asset.name
|
||||
}
|
||||
// do not emit anything else for server
|
||||
delete compilation.assets[asset.name]
|
||||
})
|
||||
|
||||
const json = JSON.stringify(bundle, null, 2)
|
||||
const filename = this.options.filename
|
||||
const src = JSON.stringify(bundle, null, 2)
|
||||
|
||||
compilation.assets[filename] = {
|
||||
source: () => json,
|
||||
size: () => json.length
|
||||
compilation.assets[this.options.filename] = {
|
||||
source: () => src,
|
||||
size: () => src.length
|
||||
}
|
||||
|
||||
cb()
|
@ -1,20 +1,21 @@
|
||||
import chalk from 'chalk'
|
||||
/**
|
||||
* This file is based on Vue.js (MIT) webpack plugins
|
||||
* https://github.com/vuejs/vue/blob/dev/src/server/webpack-plugin/util.js
|
||||
*/
|
||||
|
||||
const prefix = `[vue-server-renderer-webpack-plugin]`
|
||||
export const warn = msg => console.error(chalk.red(`${prefix} ${msg}\n`)) // eslint-disable-line no-console
|
||||
export const tip = msg => console.log(chalk.yellow(`${prefix} ${msg}\n`)) // eslint-disable-line no-console
|
||||
import consola from 'consola'
|
||||
|
||||
export const validate = (compiler) => {
|
||||
if (compiler.options.target !== 'node') {
|
||||
warn('webpack config `target` should be "node".')
|
||||
consola.warn('webpack config `target` should be "node".')
|
||||
}
|
||||
|
||||
if (compiler.options.output && compiler.options.output.libraryTarget !== 'commonjs2') {
|
||||
warn('webpack config `output.libraryTarget` should be "commonjs2".')
|
||||
consola.warn('webpack config `output.libraryTarget` should be "commonjs2".')
|
||||
}
|
||||
|
||||
if (!compiler.options.externals) {
|
||||
tip(
|
||||
consola.info(
|
||||
'It is recommended to externalize dependencies in the server build for ' +
|
||||
'better build performance.'
|
||||
)
|
@ -1,5 +1,7 @@
|
||||
export default {
|
||||
styleResources: {
|
||||
'stylus': './nothinghere'
|
||||
build: {
|
||||
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 () => {
|
||||
const opts = {
|
||||
headers: {
|
||||
|
@ -282,11 +282,6 @@ describe('basic ssr', () => {
|
||||
.rejects.toMatchObject({ statusCode: 304 })
|
||||
})
|
||||
|
||||
test('/_nuxt/server-bundle.json should return 404', async () => {
|
||||
await expect(rp(url('/_nuxt/server-bundle.json')))
|
||||
.rejects.toMatchObject({ statusCode: 404 })
|
||||
})
|
||||
|
||||
test('/_nuxt/ should return 404', async () => {
|
||||
await expect(rp(url('/_nuxt/')))
|
||||
.rejects.toMatchObject({ statusCode: 404 })
|
||||
|
@ -19,22 +19,25 @@ describe('nuxt', () => {
|
||||
expect(nuxt.initialized).toBe(true)
|
||||
})
|
||||
|
||||
test('Fail to build when no pages/ directory but is in the parent', () => {
|
||||
test('Fail to build when no pages/ directory but is in the parent', async () => {
|
||||
const config = await loadFixture('empty')
|
||||
const nuxt = new Nuxt({
|
||||
dev: false,
|
||||
...config,
|
||||
rootDir: resolve(__dirname, '..', 'fixtures', 'empty', 'pages')
|
||||
})
|
||||
|
||||
return new Builder(nuxt).build().catch((err) => {
|
||||
const s = String(err)
|
||||
expect(s).toContain('No `pages` directory found')
|
||||
expect(s).toContain('Did you mean to run `nuxt` in the parent (`../`) directory?')
|
||||
})
|
||||
try {
|
||||
await new Builder(nuxt).build()
|
||||
} catch (err) {
|
||||
expect(err.message).toContain('No `pages` directory found')
|
||||
expect(err.message).toContain('Did you mean to run `nuxt` in the parent (`../`) directory?')
|
||||
}
|
||||
expect.hasAssertions()
|
||||
})
|
||||
|
||||
test('Build with default page when no pages/ directory', async () => {
|
||||
const nuxt = new Nuxt()
|
||||
new Builder(nuxt).build()
|
||||
const config = await loadFixture('missing-pages-dir')
|
||||
const nuxt = new Nuxt(config)
|
||||
const port = await getPort()
|
||||
await nuxt.server.listen(port, 'localhost')
|
||||
|
||||
@ -45,27 +48,17 @@ describe('nuxt', () => {
|
||||
await nuxt.close()
|
||||
})
|
||||
|
||||
test('Fail to build when specified plugin isn\'t found', () => {
|
||||
const nuxt = new Nuxt({
|
||||
dev: false,
|
||||
rootDir: resolve(__dirname, '..', 'fixtures', 'missing-plugin')
|
||||
})
|
||||
test('Fail to build when specified plugin isn\'t found', async () => {
|
||||
const config = await loadFixture('missing-plugin')
|
||||
const nuxt = new Nuxt(config)
|
||||
|
||||
return new Builder(nuxt).build().catch((err) => {
|
||||
const s = String(err)
|
||||
expect(s).toContain('Plugin not found')
|
||||
})
|
||||
await expect(new Builder(nuxt).build()).rejects.toThrow('Plugin not found')
|
||||
})
|
||||
|
||||
test('Warn when styleResource isn\'t found', () => {
|
||||
const nuxt = new Nuxt({
|
||||
dev: false,
|
||||
rootDir: resolve(__dirname, '..', 'fixtures', 'missing-style-resource')
|
||||
})
|
||||
test('Warn when styleResource isn\'t found', async () => {
|
||||
const config = await loadFixture('missing-style-resource')
|
||||
const nuxt = new Nuxt(config)
|
||||
|
||||
return new Builder(nuxt).build().catch((err) => {
|
||||
const s = String(err)
|
||||
expect(s).toContain('Style Resource not found')
|
||||
})
|
||||
await expect(new Builder(nuxt).build()).rejects.toThrow('Style Resource not found')
|
||||
})
|
||||
})
|
||||
|
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 PerfLoader from '../../packages/webpack/src/config/utils/perf-loader'
|
||||
import PerfLoader from '../../packages/webpack/src/utils/perf-loader'
|
||||
|
||||
describe('webpack configuration', () => {
|
||||
test('performance loader', () => {
|
||||
|
@ -9845,7 +9845,7 @@ source-map-url@^0.4.0:
|
||||
|
||||
source-map@0.5.6:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
|
||||
resolved "http://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
|
||||
integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
|
||||
|
||||
source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1:
|
||||
|
Loading…
Reference in New Issue
Block a user