feat: loading screen (#5251)

[release]
This commit is contained in:
Sébastien Chopin 2019-03-20 10:17:53 +01:00 committed by Pooya Parsa
parent 03178b73cd
commit ef41e205e6
14 changed files with 156 additions and 190 deletions

View File

@ -58,6 +58,7 @@
"@nuxt/cli": "2.4.5",
"@nuxt/core": "2.4.5",
"@nuxt/generator": "2.4.5",
"@nuxt/loading-screen": "^0.0.2",
"@nuxt/opencollective": "^0.2.1",
"@nuxt/webpack": "2.4.5"
},

View File

@ -20,13 +20,8 @@ export default {
async run(cmd) {
const { argv } = cmd
const nuxt = await this.startDev(cmd, argv)
// Opens the server listeners url in the default browser
if (argv.open) {
const openerPromises = nuxt.server.listeners.map(listener => opener(listener.url))
await Promise.all(openerPromises)
}
await this.startDev(cmd, argv, argv.open)
},
async startDev(cmd, argv) {
@ -47,21 +42,28 @@ export default {
nuxt.hook('watch:restart', payload => this.onWatchRestart(payload, { nuxt, builder, cmd, argv }))
nuxt.hook('bundler:change', changedFileName => this.onBundlerChange(changedFileName))
// Create builder instance
const builder = await cmd.getBuilder(nuxt)
// Wait for nuxt to be ready
await nuxt.ready()
// Start listening
await nuxt.server.listen()
// Show banner when listening
showBanner(nuxt)
// Opens the server listeners url in the default browser (only once)
if (argv.open) {
argv.open = false
const openerPromises = nuxt.server.listeners.map(listener => opener(listener.url))
await Promise.all(openerPromises)
}
// Create builder instance
const builder = await cmd.getBuilder(nuxt)
// Start Build
await builder.build()
// Show banner after build
showBanner(nuxt)
// Return instance
return nuxt
},

View File

@ -30,7 +30,7 @@ export function colorize(text) {
.replace(/\[[^ ]+]/g, m => chalk.grey(m))
.replace(/<[^ ]+>/g, m => chalk.green(m))
.replace(/ (-[-\w,]+)/g, m => chalk.bold(m))
.replace(/`(.+)`/g, (_, m) => chalk.bold.cyan(m))
.replace(/`([^`]+)`/g, (_, m) => chalk.bold.cyan(m))
}
export function box(message, title, options) {

View File

@ -51,7 +51,7 @@ describe('dev', () => {
// Test error on second build so we cover oldInstance stuff
const builder = new Builder()
builder.nuxt = new Nuxt()
Builder.prototype.build = jest.fn().mockImplementationOnce(() => Promise.reject(new Error('Build Error')))
Builder.prototype.build = () => { throw new Error('Build Error') }
await Nuxt.fileRestartHook(builder)
expect(Nuxt.prototype.close).toHaveBeenCalled()

View File

@ -330,5 +330,10 @@ export function getNuxtConfig(_options) {
bundleRenderer.runInNewContext = options.dev
}
// Add loading screen
if (options.dev) {
options.devModules.push('@nuxt/loading-screen')
}
return options
}

View File

@ -12,6 +12,13 @@ export default ({ options, nuxt, renderRoute, resources }) => async function nux
const url = decodeURI(req.url)
res.statusCode = 200
const result = await renderRoute(url, context)
// If result is falsy, call renderLoading
if (!result) {
await nuxt.callHook('server:nuxt:renderLoading', req, res)
return
}
await nuxt.callHook('render:route', url, result, context)
const {
html,

View File

@ -115,6 +115,7 @@ export default class Server {
context: this.renderer.context
}))
// Dev middleware
if (this.options.dev) {
this.useMiddleware((req, res, next) => {
if (!this.devMiddleware) {

View File

@ -2,15 +2,16 @@
<div>
<section class="Landscape">
<div class="Landscape__Logo">
<div class="VueToNuxtLogo">
<div class="Triangle Triangle--two"></div>
<div class="Triangle Triangle--one"></div>
<div class="Triangle Triangle--three"></div>
<div class="Triangle Triangle--four"></div>
</div>
<h1 class="Landscape__Logo__Title">NUXT</h1>
<svg class="logo" width="220" height="166" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-18 -29)" fill="none" fill-rule="evenodd">
<path d="M0 176h67.883a22.32 22.32 0 0 1 2.453-7.296L134 57.718 100.881 0 0 176zM218.694 176H250L167.823 32 153 58.152l62.967 110.579a21.559 21.559 0 0 1 2.727 7.269z" />
<path d="M86.066 189.388a8.241 8.241 0 0 1-.443-.908 11.638 11.638 0 0 1-.792-6.566H31.976l78.55-138.174 33.05 58.932L154 94.882l-32.69-58.64C120.683 35.1 116.886 29 110.34 29c-2.959 0-7.198 1.28-10.646 7.335L20.12 176.185c-.676 1.211-3.96 7.568-.7 13.203C20.912 191.95 24.08 195 31.068 195h66.646c-6.942 0-10.156-3.004-11.647-5.612z" fill="#00C58E" />
<path d="M235.702 175.491L172.321 62.216c-.655-1.191-4.313-7.216-10.68-7.216-2.868 0-6.977 1.237-10.32 7.193L143 75.706v26.104l18.709-32.31 62.704 111.626h-23.845c.305 1.846.134 3.74-.496 5.498a7.06 7.06 0 0 1-.497 1.122l-.203.413c-3.207 5.543-10.139 5.841-11.494 5.841h37.302c1.378 0 8.287-.298 11.493-5.841 1.423-2.52 2.439-6.758-.97-12.668z" fill="#108775" />
<path d="M201.608 189.07l.21-.418c.206-.364.378-.745.515-1.139a10.94 10.94 0 0 0 .515-5.58 16.938 16.938 0 0 0-2.152-5.72l-49.733-87.006L143.5 76h-.136l-7.552 13.207-49.71 87.006a17.534 17.534 0 0 0-1.917 5.72c-.4 2.21-.148 4.486.725 6.557.13.31.278.613.444.906 1.497 2.558 4.677 5.604 11.691 5.604h92.592c1.473 0 8.651-.302 11.971-5.93zm-58.244-86.657l45.455 79.52H97.934l45.43-79.52z" fill="#2F4A5F" fill-rule="nonzero" />
</g>
</svg>
</div>
<h2 class="Landscape__Title">The Vue Framework</h2>
<h2 class="Landscape__Title">The Vue.js Framework</h2>
<a href="https://nuxtjs.org/guide/installation#starting-from-scratch" target="_blank" class="button">
Get Started
</a>
@ -85,75 +86,6 @@
}
}
.VueToNuxtLogo {
display: inline-block;
animation: turn 2s linear forwards;
transform: rotateX(180deg);
position: relative;
overflow: hidden;
height: 180px;
width: 245px;
}
.Triangle {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
}
.Triangle--one {
border-left: 105px solid transparent;
border-right: 105px solid transparent;
border-bottom: 180px solid #41B883;
}
.Triangle--two {
top: 30px;
left: 35px;
animation: goright 0.5s linear forwards 2.5s;
border-left: 87.5px solid transparent;
border-right: 87.5px solid transparent;
border-bottom: 150px solid #3B8070;
}
.Triangle--three {
top: 60px;
left: 35px;
animation: goright 0.5s linear forwards 2.5s;
border-left: 70px solid transparent;
border-right: 70px solid transparent;
border-bottom: 120px solid #35495E;
}
.Triangle--four {
top: 120px;
left: 70px;
animation: godown 0.5s linear forwards 2s;
border-left: 35px solid transparent;
border-right: 35px solid transparent;
border-bottom: 60px solid #fff;
}
@keyframes turn {
100% {
transform: rotateX(0deg);
}
}
@keyframes godown {
100% {
top: 180px;
}
}
@keyframes goright {
100% {
left: 70px;
}
}
.button {
font-family: "Quicksand", "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
position: relative;

View File

@ -1,6 +1,6 @@
<style>
body, html, #<%= globals.id %> {
background: <%= options.background %>;
background: <%= (options.theme === 'dark' ? '#2F4A5F' : 'white') %>;
width: 100%;
height: 100%;
display: flex;
@ -9,75 +9,51 @@ body, html, #<%= globals.id %> {
margin: 0;
padding: 0;
}
.logo {
animation: turn 2s linear forwards 1s;
transform: rotateX(180deg);
#nuxt-loading {
animation: opacity 1s ease-in-out;
animation-fill-mode: forwards;
animation-delay: 1s;
opacity: 0;
}
#nuxt-loading .logo {
position: relative;
height: 346px;
width: 536px;
height: 166px;
width: 220px;
}
.triangle {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
#nuxt-loading-status {
font-family: sans-serif;
font-weight: 500;
text-align: center;
color: <%= (options.theme === 'dark' ? 'white' : '#2F4A5F') %>;
}
.triangle--one {
border-left: 210px solid transparent;
border-right: 210px solid transparent;
border-bottom: 360px solid #41B883;
#nuxt-loading-status.errored {
color: <%= (options.theme === 'dark' ? '#da6371' : '#D0021B') %>;
}
.triangle--two {
top: 60px;
left: 70px;
animation: goright 0.5s linear forwards 3.5s;
border-left: 175px solid transparent;
border-right: 175px solid transparent;
border-bottom: 300px solid #3B8070;
}
.triangle--three {
top: 120px;
left: 70px;
animation: goright 0.5s linear forwards 3.5s;
border-left: 140px solid transparent;
border-right: 140px solid transparent;
border-bottom: 240px solid #35495E;
}
.triangle--four {
top: 240px;
left: 140px;
animation: godown 0.5s linear forwards 3s;
border-left: 70px solid transparent;
border-right: 70px solid transparent;
border-bottom: 120px solid #fff;
}
@keyframes turn {
@keyframes opacity {
100% {
transform: rotateX(0deg);
}
}
@keyframes godown {
100% {
top: 360px;
}
}
@keyframes goright {
100% {
left: 140px;
opacity: 1;
}
}
</style>
<div class="logo">
<div class="triangle triangle--two"></div>
<div class="triangle triangle--one"></div>
<div class="triangle triangle--three"></div>
<div class="triangle triangle--four"></div>
<div id="nuxt-loading">
<svg class="logo" width="220" height="166" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-18 -29)" fill="none" fill-rule="evenodd">
<path d="M0 176h67.883a22.32 22.32 0 0 1 2.453-7.296L134 57.718 100.881 0 0 176zM218.694 176H250L167.823 32 153 58.152l62.967 110.579a21.559 21.559 0 0 1 2.727 7.269z" />
<path d="M86.066 189.388a8.241 8.241 0 0 1-.443-.908 11.638 11.638 0 0 1-.792-6.566H31.976l78.55-138.174 33.05 58.932L154 94.882l-32.69-58.64C120.683 35.1 116.886 29 110.34 29c-2.959 0-7.198 1.28-10.646 7.335L20.12 176.185c-.676 1.211-3.96 7.568-.7 13.203C20.912 191.95 24.08 195 31.068 195h66.646c-6.942 0-10.156-3.004-11.647-5.612z" fill="#00C58E" />
<path d="M235.702 175.491L172.321 62.216c-.655-1.191-4.313-7.216-10.68-7.216-2.868 0-6.977 1.237-10.32 7.193L143 75.706v26.104l18.709-32.31 62.704 111.626h-23.845c.305 1.846.134 3.74-.496 5.498a7.06 7.06 0 0 1-.497 1.122l-.203.413c-3.207 5.543-10.139 5.841-11.494 5.841h37.302c1.378 0 8.287-.298 11.493-5.841 1.423-2.52 2.439-6.758-.97-12.668z" fill="#108775" />
<path d="M201.608 189.07l.21-.418c.206-.364.378-.745.515-1.139a10.94 10.94 0 0 0 .515-5.58 16.938 16.938 0 0 0-2.152-5.72l-49.733-87.006L143.5 76h-.136l-7.552 13.207-49.71 87.006a17.534 17.534 0 0 0-1.917 5.72c-.4 2.21-.148 4.486.725 6.557.13.31.278.613.444.906 1.497 2.558 4.677 5.604 11.691 5.604h92.592c1.473 0 8.651-.302 11.971-5.93zm-58.244-86.657l45.455 79.52H97.934l45.43-79.52z" fill="<%= (options.theme === 'dark' ? 'white' : '#2F4A5F') %>" fill-rule="nonzero" />
</g>
</svg>
<h3 id="nuxt-loading-status">Loading...</h3>
</div>
<!-- https://codepen.io/alexchopin/pen/jBWrej -->
<script>
window.addEventListener('error', function () {
var e = document.getElementById('nuxt-loading-status');
if (e) {
e.innerHTML = 'An error occurred';
e.className += 'errored'
}
});
</script>

View File

@ -5,7 +5,7 @@ import consola from 'consola'
import devalue from '@nuxt/devalue'
import invert from 'lodash/invert'
import template from 'lodash/template'
import { waitFor, isUrl, urlJoin } from '@nuxt/utils'
import { isUrl, urlJoin } from '@nuxt/utils'
import { createBundleRenderer } from 'vue-server-renderer'
import SPAMetaRenderer from './spa-meta'
@ -33,9 +33,6 @@ export default class VueRenderer {
spaTemplate: undefined,
errorTemplate: this.parseTemplate('Nuxt.js Internal Server Error')
})
// Keep time of last shown messages
this._lastWaitingForResource = new Date()
}
get assetsMapping() {
@ -46,6 +43,7 @@ export default class VueRenderer {
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]
@ -53,12 +51,14 @@ export default class VueRenderer {
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 scriptPattern = /<script[^>]*?src="([^"]*?)"[^>]*?>[^<]*?<\/script>/g
return context.renderScripts().replace(scriptPattern, (scriptTag, jsFile) => {
const legacyJsFile = jsFile.replace(this.publicPath, '')
const modernJsFile = this.assetsMapping[legacyJsFile]
@ -70,14 +70,17 @@ export default class VueRenderer {
.replace(legacyJsFile, modernJsFile)
: ''
const noModuleTag = scriptTag.replace('<script', `<script nomodule${cors}`)
return noModuleTag + moduleTag
})
}
return context.renderScripts()
}
getModernFiles(legacyFiles = []) {
const modernFiles = []
for (const legacyJsFile of legacyFiles) {
const modernFile = { ...legacyJsFile, modern: true }
if (modernFile.asType === 'script') {
@ -87,11 +90,13 @@ export default class VueRenderer {
}
modernFiles.push(modernFile)
}
return modernFiles
}
getSsrPreloadFiles(context) {
const preloadFiles = context.getPreloadFiles()
// In eligible server modern mode, preloadFiles are modern bundles from modern renderer
return this.context.options.modern === 'client' ? this.getModernFiles(preloadFiles) : preloadFiles
}
@ -99,6 +104,7 @@ export default class VueRenderer {
renderSsrResourceHints(context) {
if (this.context.options.modern === 'client') {
const linkPattern = /<link[^>]*?href="([^"]*?)"[^>]*?as="script"[^>]*?>/g
return context.renderResourceHints().replace(linkPattern, (linkTag, jsFile) => {
const legacyJsFile = jsFile.replace(this.publicPath, '')
const modernJsFile = this.assetsMapping[legacyJsFile]
@ -110,6 +116,7 @@ export default class VueRenderer {
return linkTag.replace('rel="preload"', `rel="modulepreload"${cors}`).replace(legacyJsFile, modernJsFile)
})
}
return context.renderResourceHints()
}
@ -119,6 +126,9 @@ export default class VueRenderer {
}
this._readyCalled = true
// Resolve dist path
this.distPath = path.resolve(this.context.options.buildDir, 'dist', 'server')
// -- Development mode --
if (this.context.options.dev) {
this.context.nuxt.hook('build:resources', mfs => this.loadResources(mfs))
@ -137,14 +147,13 @@ export default class VueRenderer {
}
// Verify resources
if (!this.isReady) {
if (this.context.options.modern && !this.isModernReady) {
throw new Error(
'No build files found. Use either `nuxt build` or `builder.build()` or start nuxt in development mode.'
`No modern build files found in ${this.distPath}.\nUse either \`nuxt build --modern\` or \`modern\` option to build modern files.`
)
}
if (this.context.options.modern && !this.context.resources.modernManifest) {
} else if (!this.isReady) {
throw new Error(
'No modern build files found. Use either `nuxt build --modern` or `modern` option to build modern files.'
`No build files found in ${this.distPath}.\nUse either \`nuxt build\` or \`builder.build()\` or start nuxt in development mode.`
)
}
@ -152,16 +161,17 @@ export default class VueRenderer {
}
async loadResources(_fs) {
const distPath = path.resolve(this.context.options.buildDir, 'dist', 'server')
const updated = []
const readResource = async (fileName, encoding) => {
try {
const fullPath = path.resolve(distPath, fileName)
const fullPath = path.resolve(this.distPath, fileName)
if (!await _fs.exists(fullPath)) {
return
}
const contents = await _fs.readFile(fullPath, encoding)
return contents
} catch (err) {
consola.error('Unable to load resource:', fileName, err)
@ -204,12 +214,14 @@ export default class VueRenderer {
// Call resourcesLoaded hook
consola.debug('Resources loaded:', updated.join(','))
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)
@ -217,6 +229,7 @@ export default class VueRenderer {
// 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, '')
@ -248,6 +261,10 @@ export default class VueRenderer {
return true
}
get isModernReady() {
return this.isReady && this.context.resources.modernManifest
}
// TODO: Remove in Nuxt 3
get isResourcesAvailable() { /* Backward compatibility */
return this.isReady
@ -420,25 +437,26 @@ export default class VueRenderer {
}
}
async renderRoute(url, context = {}, retries = 5) {
_throwNotReadyError() {
const error = new Error()
error.statusCode = 500
if (!this._readyCalled) {
error.message = 'Nuxt is not initialized! `nuxt.ready()` should be called.'
} else {
error.message = `SSR renderer is not initialized! Please check ${this.distPath} existence.`
}
throw error
}
async renderRoute(url, context = {}) {
/* istanbul ignore if */
if (!this.isReady) {
if (!this._readyCalled) {
throw new Error('Nuxt is not initialized! `nuxt.ready()` should be called!')
if (!this.context.options.dev) {
return this._throwNotReadyError()
}
if (!this.context.options.dev || retries <= 0) {
throw new Error('Server resources are not available!')
}
const now = new Date()
if (now - this._lastWaitingForResource > 3000) {
consola.info('Waiting for server resources...')
this._lastWaitingForResource = now
}
await waitFor(1000)
return this.renderRoute(url, context, retries - 1)
// Tell nuxt middleware to render UI
return false
}
// Log rendered url

View File

@ -385,6 +385,9 @@ export default class WebpackBaseConfig {
},
allDone: () => {
nuxt.callHook('bundler:done')
},
progress({ statesArray }) {
nuxt.callHook('bundler:progress', statesArray)
}
}
}))

View File

@ -1,6 +1,6 @@
import path from 'path'
import consola from 'consola'
import { Builder, BundleBuilder, getPort, loadFixture, Nuxt, rp } from '../utils'
import { Builder, BundleBuilder, getPort, loadFixture, Nuxt, rp, waitFor } from '../utils'
let port
const url = route => 'http://localhost:' + port + route
@ -50,9 +50,15 @@ describe('basic dev', () => {
}
}
})
nuxt = new Nuxt(config)
await nuxt.ready()
builder = new Builder(nuxt, BundleBuilder)
await builder.build()
await waitFor(2000) // TODO: Find a better way
port = await getPort()
await nuxt.server.listen(port, 'localhost')
})

View File

@ -1,9 +1,8 @@
import path from 'path'
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.'
const NO_MODERN_BUILD_MSG = 'No modern build files found. Use either `nuxt build --modern` or `modern` option to build modern files.'
const NO_BUILD_MSG = /Use either `nuxt build` or `builder\.build\(\)` or start nuxt in development mode/
const NO_MODERN_BUILD_MSG = /Use either `nuxt build --modern` or `modern` option to build modern files/
describe('renderer', () => {
afterEach(() => {
@ -19,7 +18,9 @@ describe('renderer', () => {
})
await nuxt.ready()
await expect(nuxt.renderer.renderer.isReady).toBe(false)
expect(consola.fatal).toHaveBeenCalledWith(new Error(NO_BUILD_MSG))
expect(consola.fatal).toHaveBeenCalledWith(expect.objectContaining({
message: expect.stringMatching(NO_BUILD_MSG)
}))
})
test('detect no-build (SPA)', async () => {
@ -31,7 +32,9 @@ describe('renderer', () => {
})
await nuxt.ready()
await expect(nuxt.renderer.renderer.isReady).toBe(false)
expect(consola.fatal).toHaveBeenCalledWith(new Error(NO_BUILD_MSG))
expect(consola.fatal).toHaveBeenCalledWith(expect.objectContaining({
message: expect.stringMatching(NO_BUILD_MSG)
}))
})
test('detect no-modern-build', async () => {
const nuxt = new Nuxt({
@ -39,10 +42,12 @@ describe('renderer', () => {
mode: 'universal',
modern: 'client',
dev: false,
buildDir: path.resolve(__dirname, '..', 'fixtures', 'empty', '.nuxt')
buildDir: '/path/to/404'
})
await nuxt.ready()
await expect(nuxt.renderer.renderer.isReady).toBe(true)
expect(consola.fatal).toHaveBeenCalledWith(new Error(NO_MODERN_BUILD_MSG))
await expect(nuxt.renderer.renderer.isModernReady).toBe(false)
expect(consola.fatal).toHaveBeenCalledWith(expect.objectContaining({
message: expect.stringMatching(NO_MODERN_BUILD_MSG)
}))
})
})

View File

@ -1585,6 +1585,16 @@
error-stack-parser "^2.0.0"
string-width "^2.0.0"
"@nuxt/loading-screen@^0.0.2":
version "0.0.2"
resolved "https://registry.npmjs.org/@nuxt/loading-screen/-/loading-screen-0.0.2.tgz#c3ba3b408c4af93998b1a99c33dddc4b6e3114e9"
integrity sha512-/DDpjzlLxlTciVafjF5VS5oJFZAirXryzTfT6yQE/rSl0bEFOsa5Xm/N+NYoHmnp3HfYyYfl7IzOVBLQ5IUM8w==
dependencies:
connect "^3.6.6"
fs-extra "^7.0.1"
serve-static "^1.13.2"
ws "^6.2.0"
"@nuxt/opencollective@^0.2.1":
version "0.2.1"
resolved "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.2.1.tgz#8290f1220072637e575c3935733719a78ad2d056"
@ -11733,7 +11743,7 @@ ws@^5.2.0:
dependencies:
async-limiter "~1.0.0"
ws@^6.0.0, ws@^6.1.0, ws@^6.1.2:
ws@^6.0.0, ws@^6.1.0, ws@^6.1.2, ws@^6.2.0:
version "6.2.0"
resolved "https://registry.npmjs.org/ws/-/ws-6.2.0.tgz#13806d9913b2a5f3cbb9ba47b563c002cbc7c526"
integrity sha512-deZYUNlt2O4buFCa3t5bKLf8A7FPP/TVjwOeVNpw818Ma5nk4MLXls2eoEGS39o8119QIYxTrTDoPQ5B/gTD6w==