Migrate server-sent events HMR connection to WebSocket (#29903)
This replaces the server-sent events HMR connection with a WebSocket connection to prevent hitting browser connection limits, allow sending events back from the browser, and overall better performance. This approach sets up the the `upgrade` event listener on the server immediately when created via `next dev` and on the first request using `req.socket.server` when created via a custom server. In a follow-up PR we can push the files changed via the WebSocket as well. x-ref: https://github.com/vercel/next.js/issues/10061 x-ref: https://github.com/vercel/next.js/issues/8064 x-ref: https://github.com/vercel/next.js/issues/4495 ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes
This commit is contained in:
parent
ea0cdc5a36
commit
75748caf7f
27 changed files with 289 additions and 1247 deletions
|
@ -1,12 +1,7 @@
|
|||
/* globals __webpack_hash__ */
|
||||
import EventSourcePolyfill from './event-source-polyfill'
|
||||
import { addMessageListener } from './error-overlay/eventsource'
|
||||
import { setupPing } from './on-demand-entries-utils'
|
||||
import { displayContent } from './fouc'
|
||||
|
||||
if (!window.EventSource) {
|
||||
window.EventSource = EventSourcePolyfill
|
||||
}
|
||||
import initOnDemandEntries from './on-demand-entries-client'
|
||||
import { addMessageListener, connectHMR } from './error-overlay/websocket'
|
||||
|
||||
const data = JSON.parse(document.getElementById('__NEXT_DATA__').textContent)
|
||||
let { assetPrefix, page } = data
|
||||
|
@ -94,5 +89,6 @@ addMessageListener((event) => {
|
|||
}
|
||||
})
|
||||
|
||||
setupPing(assetPrefix, () => page)
|
||||
connectHMR({ path: `${assetPrefix}/_next/webpack-hmr` })
|
||||
displayContent()
|
||||
initOnDemandEntries()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { addMessageListener } from './error-overlay/eventsource'
|
||||
import { addMessageListener } from './error-overlay/websocket'
|
||||
|
||||
export default function initializeBuildWatcher(toggleCallback) {
|
||||
const shadowHost = document.createElement('div')
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
const eventCallbacks = []
|
||||
|
||||
function EventSourceWrapper(options) {
|
||||
var source
|
||||
var lastActivity = new Date()
|
||||
var listeners = []
|
||||
|
||||
if (!options.timeout) {
|
||||
options.timeout = 20 * 1000
|
||||
}
|
||||
|
||||
init()
|
||||
var timer = setInterval(function () {
|
||||
if (new Date() - lastActivity > options.timeout) {
|
||||
handleDisconnect()
|
||||
}
|
||||
}, options.timeout / 2)
|
||||
|
||||
function init() {
|
||||
source = new window.EventSource(options.path)
|
||||
source.onopen = handleOnline
|
||||
source.onerror = handleDisconnect
|
||||
source.onmessage = handleMessage
|
||||
}
|
||||
|
||||
function handleOnline() {
|
||||
if (options.log) console.log('[HMR] connected')
|
||||
lastActivity = new Date()
|
||||
}
|
||||
|
||||
function handleMessage(event) {
|
||||
lastActivity = new Date()
|
||||
for (var i = 0; i < listeners.length; i++) {
|
||||
listeners[i](event)
|
||||
}
|
||||
|
||||
eventCallbacks.forEach((cb) => {
|
||||
if (!cb.unfiltered && event.data.indexOf('action') === -1) return
|
||||
cb(event)
|
||||
})
|
||||
}
|
||||
|
||||
function handleDisconnect() {
|
||||
clearInterval(timer)
|
||||
source.close()
|
||||
setTimeout(init, options.timeout)
|
||||
}
|
||||
|
||||
return {
|
||||
close: () => {
|
||||
clearInterval(timer)
|
||||
source.close()
|
||||
},
|
||||
addMessageListener: function (fn) {
|
||||
listeners.push(fn)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function addMessageListener(cb) {
|
||||
eventCallbacks.push(cb)
|
||||
}
|
||||
|
||||
export function getEventSourceWrapper(options) {
|
||||
return EventSourceWrapper(options)
|
||||
}
|
|
@ -33,7 +33,7 @@ import {
|
|||
onRefresh,
|
||||
} from '@next/react-dev-overlay/lib/client'
|
||||
import stripAnsi from 'next/dist/compiled/strip-ansi'
|
||||
import { addMessageListener } from './eventsource'
|
||||
import { addMessageListener } from './websocket'
|
||||
import formatWebpackMessages from './format-webpack-messages'
|
||||
|
||||
// This alternative WebpackDevServer combines the functionality of:
|
||||
|
@ -51,10 +51,8 @@ export default function connect() {
|
|||
register()
|
||||
|
||||
addMessageListener((event) => {
|
||||
// This is the heartbeat event
|
||||
if (event.data === '\uD83D\uDC93') {
|
||||
return
|
||||
}
|
||||
if (event.data.indexOf('action') === -1) return
|
||||
|
||||
try {
|
||||
processMessage(event)
|
||||
} catch (ex) {
|
||||
|
|
59
packages/next/client/dev/error-overlay/websocket.ts
Normal file
59
packages/next/client/dev/error-overlay/websocket.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
let source: WebSocket
|
||||
const eventCallbacks: ((event: any) => void)[] = []
|
||||
let lastActivity = Date.now()
|
||||
|
||||
export function addMessageListener(cb: (event: any) => void) {
|
||||
eventCallbacks.push(cb)
|
||||
}
|
||||
|
||||
export function sendMessage(data: any) {
|
||||
if (!source || source.readyState !== source.OPEN) return
|
||||
return source.send(data)
|
||||
}
|
||||
|
||||
export function connectHMR(options: {
|
||||
path: string
|
||||
timeout: number
|
||||
log?: boolean
|
||||
}) {
|
||||
if (!options.timeout) {
|
||||
options.timeout = 5 * 1000
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
let timer = setInterval(function () {
|
||||
if (Date.now() - lastActivity > options.timeout) {
|
||||
handleDisconnect()
|
||||
}
|
||||
}, options.timeout / 2)
|
||||
|
||||
function init() {
|
||||
if (source) source.close()
|
||||
const { protocol, hostname, port } = location
|
||||
const url = `${protocol === 'http:' ? 'ws' : 'wss'}://${hostname}:${port}`
|
||||
source = new window.WebSocket(`${url}${options.path}`)
|
||||
source.onopen = handleOnline
|
||||
source.onerror = handleDisconnect
|
||||
source.onmessage = handleMessage
|
||||
}
|
||||
|
||||
function handleOnline() {
|
||||
if (options.log) console.log('[HMR] connected')
|
||||
lastActivity = Date.now()
|
||||
}
|
||||
|
||||
function handleMessage(event: any) {
|
||||
lastActivity = Date.now()
|
||||
|
||||
eventCallbacks.forEach((cb) => {
|
||||
cb(event)
|
||||
})
|
||||
}
|
||||
|
||||
function handleDisconnect() {
|
||||
clearInterval(timer)
|
||||
source.close()
|
||||
setTimeout(init, options.timeout)
|
||||
}
|
||||
}
|
|
@ -1,964 +0,0 @@
|
|||
/* eslint-disable */
|
||||
// Improved version of https://github.com/Yaffle/EventSource/
|
||||
// Available under MIT License (MIT)
|
||||
// Only tries to support IE11 and nothing below
|
||||
var document = window.document
|
||||
var Response = window.Response
|
||||
var TextDecoder = window.TextDecoder
|
||||
var TextEncoder = window.TextEncoder
|
||||
var AbortController = window.AbortController
|
||||
|
||||
if (AbortController == undefined) {
|
||||
AbortController = function () {
|
||||
this.signal = null
|
||||
this.abort = function () {}
|
||||
}
|
||||
}
|
||||
|
||||
function TextDecoderPolyfill() {
|
||||
this.bitsNeeded = 0
|
||||
this.codePoint = 0
|
||||
}
|
||||
|
||||
TextDecoderPolyfill.prototype.decode = function (octets) {
|
||||
function valid(codePoint, shift, octetsCount) {
|
||||
if (octetsCount === 1) {
|
||||
return codePoint >= 0x0080 >> shift && codePoint << shift <= 0x07ff
|
||||
}
|
||||
if (octetsCount === 2) {
|
||||
return (
|
||||
(codePoint >= 0x0800 >> shift && codePoint << shift <= 0xd7ff) ||
|
||||
(codePoint >= 0xe000 >> shift && codePoint << shift <= 0xffff)
|
||||
)
|
||||
}
|
||||
if (octetsCount === 3) {
|
||||
return codePoint >= 0x010000 >> shift && codePoint << shift <= 0x10ffff
|
||||
}
|
||||
throw new Error()
|
||||
}
|
||||
function octetsCount(bitsNeeded, codePoint) {
|
||||
if (bitsNeeded === 6 * 1) {
|
||||
return codePoint >> 6 > 15 ? 3 : codePoint > 31 ? 2 : 1
|
||||
}
|
||||
if (bitsNeeded === 6 * 2) {
|
||||
return codePoint > 15 ? 3 : 2
|
||||
}
|
||||
if (bitsNeeded === 6 * 3) {
|
||||
return 3
|
||||
}
|
||||
throw new Error()
|
||||
}
|
||||
var REPLACER = 0xfffd
|
||||
var string = ''
|
||||
var bitsNeeded = this.bitsNeeded
|
||||
var codePoint = this.codePoint
|
||||
for (var i = 0; i < octets.length; i += 1) {
|
||||
var octet = octets[i]
|
||||
if (bitsNeeded !== 0) {
|
||||
if (
|
||||
octet < 128 ||
|
||||
octet > 191 ||
|
||||
!valid(
|
||||
(codePoint << 6) | (octet & 63),
|
||||
bitsNeeded - 6,
|
||||
octetsCount(bitsNeeded, codePoint)
|
||||
)
|
||||
) {
|
||||
bitsNeeded = 0
|
||||
codePoint = REPLACER
|
||||
string += String.fromCharCode(codePoint)
|
||||
}
|
||||
}
|
||||
if (bitsNeeded === 0) {
|
||||
if (octet >= 0 && octet <= 127) {
|
||||
bitsNeeded = 0
|
||||
codePoint = octet
|
||||
} else if (octet >= 192 && octet <= 223) {
|
||||
bitsNeeded = 6 * 1
|
||||
codePoint = octet & 31
|
||||
} else if (octet >= 224 && octet <= 239) {
|
||||
bitsNeeded = 6 * 2
|
||||
codePoint = octet & 15
|
||||
} else if (octet >= 240 && octet <= 247) {
|
||||
bitsNeeded = 6 * 3
|
||||
codePoint = octet & 7
|
||||
} else {
|
||||
bitsNeeded = 0
|
||||
codePoint = REPLACER
|
||||
}
|
||||
if (
|
||||
bitsNeeded !== 0 &&
|
||||
!valid(codePoint, bitsNeeded, octetsCount(bitsNeeded, codePoint))
|
||||
) {
|
||||
bitsNeeded = 0
|
||||
codePoint = REPLACER
|
||||
}
|
||||
} else {
|
||||
bitsNeeded -= 6
|
||||
codePoint = (codePoint << 6) | (octet & 63)
|
||||
}
|
||||
if (bitsNeeded === 0) {
|
||||
if (codePoint <= 0xffff) {
|
||||
string += String.fromCharCode(codePoint)
|
||||
} else {
|
||||
string += String.fromCharCode(0xd800 + ((codePoint - 0xffff - 1) >> 10))
|
||||
string += String.fromCharCode(
|
||||
0xdc00 + ((codePoint - 0xffff - 1) & 0x3ff)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.bitsNeeded = bitsNeeded
|
||||
this.codePoint = codePoint
|
||||
return string
|
||||
}
|
||||
|
||||
// Firefox < 38 throws an error with stream option
|
||||
var supportsStreamOption = function () {
|
||||
try {
|
||||
return (
|
||||
new TextDecoder().decode(new TextEncoder().encode('test'), {
|
||||
stream: true,
|
||||
}) === 'test'
|
||||
)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IE, Edge
|
||||
if (
|
||||
TextDecoder == undefined ||
|
||||
TextEncoder == undefined ||
|
||||
!supportsStreamOption()
|
||||
) {
|
||||
TextDecoder = TextDecoderPolyfill
|
||||
}
|
||||
|
||||
var k = function () {}
|
||||
|
||||
function XHRWrapper(xhr) {
|
||||
this.withCredentials = false
|
||||
this.responseType = ''
|
||||
this.readyState = 0
|
||||
this.status = 0
|
||||
this.statusText = ''
|
||||
this.responseText = ''
|
||||
this.onprogress = k
|
||||
this.onreadystatechange = k
|
||||
this._contentType = ''
|
||||
this._xhr = xhr
|
||||
this._sendTimeout = 0
|
||||
this._abort = k
|
||||
}
|
||||
|
||||
XHRWrapper.prototype.open = function (method, url) {
|
||||
this._abort(true)
|
||||
|
||||
var that = this
|
||||
var xhr = this._xhr
|
||||
var state = 1
|
||||
var timeout = 0
|
||||
|
||||
this._abort = function (silent) {
|
||||
if (that._sendTimeout !== 0) {
|
||||
clearTimeout(that._sendTimeout)
|
||||
that._sendTimeout = 0
|
||||
}
|
||||
if (state === 1 || state === 2 || state === 3) {
|
||||
state = 4
|
||||
xhr.onload = k
|
||||
xhr.onerror = k
|
||||
xhr.onabort = k
|
||||
xhr.onprogress = k
|
||||
xhr.onreadystatechange = k
|
||||
// IE 8 - 9: XDomainRequest#abort() does not fire any event
|
||||
// Opera < 10: XMLHttpRequest#abort() does not fire any event
|
||||
xhr.abort()
|
||||
if (timeout !== 0) {
|
||||
clearTimeout(timeout)
|
||||
timeout = 0
|
||||
}
|
||||
if (!silent) {
|
||||
that.readyState = 4
|
||||
that.onreadystatechange()
|
||||
}
|
||||
}
|
||||
state = 0
|
||||
}
|
||||
|
||||
var onStart = function () {
|
||||
if (state === 1) {
|
||||
// state = 2;
|
||||
var status = 0
|
||||
var statusText = ''
|
||||
var contentType = undefined
|
||||
if (!('contentType' in xhr)) {
|
||||
try {
|
||||
status = xhr.status
|
||||
statusText = xhr.statusText
|
||||
contentType = xhr.getResponseHeader('Content-Type')
|
||||
} catch (error) {
|
||||
// IE < 10 throws exception for `xhr.status` when xhr.readyState === 2 || xhr.readyState === 3
|
||||
// Opera < 11 throws exception for `xhr.status` when xhr.readyState === 2
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=29121
|
||||
status = 0
|
||||
statusText = ''
|
||||
contentType = undefined
|
||||
// Firefox < 14, Chrome ?, Safari ?
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=29658
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=77854
|
||||
}
|
||||
} else {
|
||||
status = 200
|
||||
statusText = 'OK'
|
||||
contentType = xhr.contentType
|
||||
}
|
||||
if (status !== 0) {
|
||||
state = 2
|
||||
that.readyState = 2
|
||||
that.status = status
|
||||
that.statusText = statusText
|
||||
that._contentType = contentType
|
||||
that.onreadystatechange()
|
||||
}
|
||||
}
|
||||
}
|
||||
var onProgress = function () {
|
||||
onStart()
|
||||
if (state === 2 || state === 3) {
|
||||
state = 3
|
||||
var responseText = ''
|
||||
try {
|
||||
responseText = xhr.responseText
|
||||
} catch (error) {
|
||||
// IE 8 - 9 with XMLHttpRequest
|
||||
}
|
||||
that.readyState = 3
|
||||
that.responseText = responseText
|
||||
that.onprogress()
|
||||
}
|
||||
}
|
||||
var onFinish = function () {
|
||||
// Firefox 52 fires "readystatechange" (xhr.readyState === 4) without final "readystatechange" (xhr.readyState === 3)
|
||||
// IE 8 fires "onload" without "onprogress"
|
||||
onProgress()
|
||||
if (state === 1 || state === 2 || state === 3) {
|
||||
state = 4
|
||||
if (timeout !== 0) {
|
||||
clearTimeout(timeout)
|
||||
timeout = 0
|
||||
}
|
||||
that.readyState = 4
|
||||
that.onreadystatechange()
|
||||
}
|
||||
}
|
||||
var onReadyStateChange = function () {
|
||||
if (xhr != undefined) {
|
||||
// Opera 12
|
||||
if (xhr.readyState === 4) {
|
||||
onFinish()
|
||||
} else if (xhr.readyState === 3) {
|
||||
onProgress()
|
||||
} else if (xhr.readyState === 2) {
|
||||
onStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
var onTimeout = function () {
|
||||
timeout = setTimeout(function () {
|
||||
onTimeout()
|
||||
}, 500)
|
||||
if (xhr.readyState === 3) {
|
||||
onProgress()
|
||||
}
|
||||
}
|
||||
|
||||
// XDomainRequest#abort removes onprogress, onerror, onload
|
||||
xhr.onload = onFinish
|
||||
xhr.onerror = onFinish
|
||||
// improper fix to match Firefox behavior, but it is better than just ignore abort
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=768596
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=880200
|
||||
// https://code.google.com/p/chromium/issues/detail?id=153570
|
||||
// IE 8 fires "onload" without "onprogress
|
||||
xhr.onabort = onFinish
|
||||
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=736723
|
||||
if (
|
||||
!('sendAsBinary' in XMLHttpRequest.prototype) &&
|
||||
!('mozAnon' in XMLHttpRequest.prototype)
|
||||
) {
|
||||
xhr.onprogress = onProgress
|
||||
}
|
||||
|
||||
// IE 8 - 9 (XMLHTTPRequest)
|
||||
// Opera < 12
|
||||
// Firefox < 3.5
|
||||
// Firefox 3.5 - 3.6 - ? < 9.0
|
||||
// onprogress is not fired sometimes or delayed
|
||||
// see also #64
|
||||
xhr.onreadystatechange = onReadyStateChange
|
||||
|
||||
if ('contentType' in xhr) {
|
||||
url += (url.indexOf('?') === -1 ? '?' : '&') + 'padding=true'
|
||||
}
|
||||
xhr.open(method, url, true)
|
||||
|
||||
if ('readyState' in xhr) {
|
||||
// workaround for Opera 12 issue with "progress" events
|
||||
// #91
|
||||
timeout = setTimeout(function () {
|
||||
onTimeout()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
XHRWrapper.prototype.abort = function () {
|
||||
this._abort(false)
|
||||
}
|
||||
XHRWrapper.prototype.getResponseHeader = function (name) {
|
||||
return this._contentType
|
||||
}
|
||||
XHRWrapper.prototype.setRequestHeader = function (name, value) {
|
||||
var xhr = this._xhr
|
||||
if ('setRequestHeader' in xhr) {
|
||||
xhr.setRequestHeader(name, value)
|
||||
}
|
||||
}
|
||||
XHRWrapper.prototype.getAllResponseHeaders = function () {
|
||||
return this._xhr.getAllResponseHeaders != undefined
|
||||
? this._xhr.getAllResponseHeaders()
|
||||
: ''
|
||||
}
|
||||
XHRWrapper.prototype.send = function () {
|
||||
// loading indicator in Safari < ? (6), Chrome < 14, Firefox
|
||||
if (
|
||||
!('ontimeout' in XMLHttpRequest.prototype) &&
|
||||
document != undefined &&
|
||||
document.readyState != undefined &&
|
||||
document.readyState !== 'complete'
|
||||
) {
|
||||
var that = this
|
||||
that._sendTimeout = setTimeout(function () {
|
||||
that._sendTimeout = 0
|
||||
that.send()
|
||||
}, 4)
|
||||
return
|
||||
}
|
||||
|
||||
var xhr = this._xhr
|
||||
// withCredentials should be set after "open" for Safari and Chrome (< 19 ?)
|
||||
xhr.withCredentials = this.withCredentials
|
||||
xhr.responseType = this.responseType
|
||||
try {
|
||||
// xhr.send(); throws "Not enough arguments" in Firefox 3.0
|
||||
xhr.send(undefined)
|
||||
} catch (error1) {
|
||||
// Safari 5.1.7, Opera 12
|
||||
throw error1
|
||||
}
|
||||
}
|
||||
|
||||
function toLowerCase(name) {
|
||||
return name.replace(/[A-Z]/g, function (c) {
|
||||
return String.fromCharCode(c.charCodeAt(0) + 0x20)
|
||||
})
|
||||
}
|
||||
|
||||
function HeadersPolyfill(all) {
|
||||
// Get headers: implemented according to mozilla's example code: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getAllResponseHeaders#Example
|
||||
var map = Object.create(null)
|
||||
var array = all.split('\r\n')
|
||||
for (var i = 0; i < array.length; i += 1) {
|
||||
var line = array[i]
|
||||
var parts = line.split(': ')
|
||||
var name = parts.shift()
|
||||
var value = parts.join(': ')
|
||||
map[toLowerCase(name)] = value
|
||||
}
|
||||
this._map = map
|
||||
}
|
||||
HeadersPolyfill.prototype.get = function (name) {
|
||||
return this._map[toLowerCase(name)]
|
||||
}
|
||||
|
||||
function XHRTransport() {}
|
||||
|
||||
XHRTransport.prototype.open = function (
|
||||
xhr,
|
||||
onStartCallback,
|
||||
onProgressCallback,
|
||||
onFinishCallback,
|
||||
url,
|
||||
withCredentials,
|
||||
headers
|
||||
) {
|
||||
xhr.open('GET', url)
|
||||
var offset = 0
|
||||
xhr.onprogress = function () {
|
||||
var responseText = xhr.responseText
|
||||
var chunk = responseText.slice(offset)
|
||||
offset += chunk.length
|
||||
onProgressCallback(chunk)
|
||||
}
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 2) {
|
||||
var status = xhr.status
|
||||
var statusText = xhr.statusText
|
||||
var contentType = xhr.getResponseHeader('Content-Type')
|
||||
var headers = xhr.getAllResponseHeaders()
|
||||
onStartCallback(
|
||||
status,
|
||||
statusText,
|
||||
contentType,
|
||||
new HeadersPolyfill(headers),
|
||||
function () {
|
||||
xhr.abort()
|
||||
}
|
||||
)
|
||||
} else if (xhr.readyState === 4) {
|
||||
onFinishCallback()
|
||||
}
|
||||
}
|
||||
xhr.withCredentials = withCredentials
|
||||
xhr.responseType = 'text'
|
||||
for (var name in headers) {
|
||||
if (Object.prototype.hasOwnProperty.call(headers, name)) {
|
||||
xhr.setRequestHeader(name, headers[name])
|
||||
}
|
||||
}
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
function HeadersWrapper(headers) {
|
||||
this._headers = headers
|
||||
}
|
||||
HeadersWrapper.prototype.get = function (name) {
|
||||
return this._headers.get(name)
|
||||
}
|
||||
|
||||
function FetchTransport() {}
|
||||
|
||||
FetchTransport.prototype.open = function (
|
||||
xhr,
|
||||
onStartCallback,
|
||||
onProgressCallback,
|
||||
onFinishCallback,
|
||||
url,
|
||||
withCredentials,
|
||||
headers
|
||||
) {
|
||||
var controller = new AbortController()
|
||||
var signal = controller.signal // see #120
|
||||
var textDecoder = new TextDecoder()
|
||||
fetch(url, {
|
||||
headers: headers,
|
||||
credentials: withCredentials ? 'include' : 'same-origin',
|
||||
signal: signal,
|
||||
cache: 'no-store',
|
||||
})
|
||||
.then(function (response) {
|
||||
var reader = response.body.getReader()
|
||||
onStartCallback(
|
||||
response.status,
|
||||
response.statusText,
|
||||
response.headers.get('Content-Type'),
|
||||
new HeadersWrapper(response.headers),
|
||||
function () {
|
||||
controller.abort()
|
||||
reader.cancel()
|
||||
}
|
||||
)
|
||||
return new Promise(function (resolve, reject) {
|
||||
var readNextChunk = function () {
|
||||
reader
|
||||
.read()
|
||||
.then(function (result) {
|
||||
if (result.done) {
|
||||
// Note: bytes in textDecoder are ignored
|
||||
resolve(undefined)
|
||||
} else {
|
||||
var chunk = textDecoder.decode(result.value, { stream: true })
|
||||
onProgressCallback(chunk)
|
||||
readNextChunk()
|
||||
}
|
||||
})
|
||||
['catch'](function (error) {
|
||||
reject(error)
|
||||
})
|
||||
}
|
||||
readNextChunk()
|
||||
})
|
||||
})
|
||||
.then(
|
||||
function (result) {
|
||||
onFinishCallback()
|
||||
return result
|
||||
},
|
||||
function (error) {
|
||||
onFinishCallback()
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function EventTarget() {
|
||||
this._listeners = Object.create(null)
|
||||
}
|
||||
|
||||
function throwError(e) {
|
||||
setTimeout(function () {
|
||||
throw e
|
||||
}, 0)
|
||||
}
|
||||
|
||||
EventTarget.prototype.dispatchEvent = function (event) {
|
||||
event.target = this
|
||||
var typeListeners = this._listeners[event.type]
|
||||
if (typeListeners != undefined) {
|
||||
var length = typeListeners.length
|
||||
for (var i = 0; i < length; i += 1) {
|
||||
var listener = typeListeners[i]
|
||||
try {
|
||||
if (typeof listener.handleEvent === 'function') {
|
||||
listener.handleEvent(event)
|
||||
} else {
|
||||
listener.call(this, event)
|
||||
}
|
||||
} catch (e) {
|
||||
throwError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EventTarget.prototype.addEventListener = function (type, listener) {
|
||||
type = String(type)
|
||||
var listeners = this._listeners
|
||||
var typeListeners = listeners[type]
|
||||
if (typeListeners == undefined) {
|
||||
typeListeners = []
|
||||
listeners[type] = typeListeners
|
||||
}
|
||||
var found = false
|
||||
for (var i = 0; i < typeListeners.length; i += 1) {
|
||||
if (typeListeners[i] === listener) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
typeListeners.push(listener)
|
||||
}
|
||||
}
|
||||
EventTarget.prototype.removeEventListener = function (type, listener) {
|
||||
type = String(type)
|
||||
var listeners = this._listeners
|
||||
var typeListeners = listeners[type]
|
||||
if (typeListeners != undefined) {
|
||||
var filtered = []
|
||||
for (var i = 0; i < typeListeners.length; i += 1) {
|
||||
if (typeListeners[i] !== listener) {
|
||||
filtered.push(typeListeners[i])
|
||||
}
|
||||
}
|
||||
if (filtered.length === 0) {
|
||||
delete listeners[type]
|
||||
} else {
|
||||
listeners[type] = filtered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Event(type) {
|
||||
this.type = type
|
||||
this.target = undefined
|
||||
}
|
||||
|
||||
function MessageEvent(type, options) {
|
||||
Event.call(this, type)
|
||||
this.data = options.data
|
||||
this.lastEventId = options.lastEventId
|
||||
}
|
||||
|
||||
MessageEvent.prototype = Object.create(Event.prototype)
|
||||
|
||||
function ConnectionEvent(type, options) {
|
||||
Event.call(this, type)
|
||||
this.status = options.status
|
||||
this.statusText = options.statusText
|
||||
this.headers = options.headers
|
||||
}
|
||||
|
||||
ConnectionEvent.prototype = Object.create(Event.prototype)
|
||||
|
||||
var WAITING = -1
|
||||
var CONNECTING = 0
|
||||
var OPEN = 1
|
||||
var CLOSED = 2
|
||||
|
||||
var AFTER_CR = -1
|
||||
var FIELD_START = 0
|
||||
var FIELD = 1
|
||||
var VALUE_START = 2
|
||||
var VALUE = 3
|
||||
|
||||
var contentTypeRegExp = /^text\/event\-stream;?(\s*charset\=utf\-8)?$/i
|
||||
|
||||
var MINIMUM_DURATION = 1000
|
||||
var MAXIMUM_DURATION = 18000000
|
||||
|
||||
var parseDuration = function (value, def) {
|
||||
var n = parseInt(value, 10)
|
||||
if (n !== n) {
|
||||
n = def
|
||||
}
|
||||
return clampDuration(n)
|
||||
}
|
||||
var clampDuration = function (n) {
|
||||
return Math.min(Math.max(n, MINIMUM_DURATION), MAXIMUM_DURATION)
|
||||
}
|
||||
|
||||
var fire = function (that, f, event) {
|
||||
try {
|
||||
if (typeof f === 'function') {
|
||||
f.call(that, event)
|
||||
}
|
||||
} catch (e) {
|
||||
throwError(e)
|
||||
}
|
||||
}
|
||||
|
||||
function EventSourcePolyfill(url, options) {
|
||||
EventTarget.call(this)
|
||||
|
||||
this.onopen = undefined
|
||||
this.onmessage = undefined
|
||||
this.onerror = undefined
|
||||
|
||||
this.url = undefined
|
||||
this.readyState = undefined
|
||||
this.withCredentials = undefined
|
||||
|
||||
this._close = undefined
|
||||
|
||||
start(this, url, options)
|
||||
}
|
||||
|
||||
var isFetchSupported =
|
||||
fetch != undefined && Response != undefined && 'body' in Response.prototype
|
||||
|
||||
function start(es, url, options) {
|
||||
url = String(url)
|
||||
var withCredentials = options != undefined && Boolean(options.withCredentials)
|
||||
|
||||
var initialRetry = clampDuration(1000)
|
||||
var heartbeatTimeout =
|
||||
options != undefined && options.heartbeatTimeout != undefined
|
||||
? parseDuration(options.heartbeatTimeout, 45000)
|
||||
: clampDuration(45000)
|
||||
|
||||
var lastEventId = ''
|
||||
var retry = initialRetry
|
||||
var wasActivity = false
|
||||
var headers =
|
||||
options != undefined && options.headers != undefined
|
||||
? JSON.parse(JSON.stringify(options.headers))
|
||||
: undefined
|
||||
var CurrentTransport =
|
||||
options != undefined && options.Transport != undefined
|
||||
? options.Transport
|
||||
: XMLHttpRequest
|
||||
var xhr =
|
||||
isFetchSupported &&
|
||||
!(options != undefined && options.Transport != undefined)
|
||||
? undefined
|
||||
: new XHRWrapper(new CurrentTransport())
|
||||
var transport = xhr == undefined ? new FetchTransport() : new XHRTransport()
|
||||
var cancelFunction = undefined
|
||||
var timeout = 0
|
||||
var currentState = WAITING
|
||||
var dataBuffer = ''
|
||||
var lastEventIdBuffer = ''
|
||||
var eventTypeBuffer = ''
|
||||
|
||||
var textBuffer = ''
|
||||
var state = FIELD_START
|
||||
var fieldStart = 0
|
||||
var valueStart = 0
|
||||
|
||||
var onStart = function (status, statusText, contentType, headers, cancel) {
|
||||
if (currentState === CONNECTING) {
|
||||
cancelFunction = cancel
|
||||
if (
|
||||
status === 200 &&
|
||||
contentType != undefined &&
|
||||
contentTypeRegExp.test(contentType)
|
||||
) {
|
||||
currentState = OPEN
|
||||
wasActivity = true
|
||||
retry = initialRetry
|
||||
es.readyState = OPEN
|
||||
var event = new ConnectionEvent('open', {
|
||||
status: status,
|
||||
statusText: statusText,
|
||||
headers: headers,
|
||||
})
|
||||
es.dispatchEvent(event)
|
||||
fire(es, es.onopen, event)
|
||||
} else {
|
||||
var message = ''
|
||||
if (status !== 200) {
|
||||
if (statusText) {
|
||||
statusText = statusText.replace(/\s+/g, ' ')
|
||||
}
|
||||
message =
|
||||
"EventSource's response has a status " +
|
||||
status +
|
||||
' ' +
|
||||
statusText +
|
||||
' that is not 200. Aborting the connection.'
|
||||
} else {
|
||||
message =
|
||||
"EventSource's response has a Content-Type specifying an unsupported type: " +
|
||||
(contentType == undefined
|
||||
? '-'
|
||||
: contentType.replace(/\s+/g, ' ')) +
|
||||
'. Aborting the connection.'
|
||||
}
|
||||
throwError(new Error(message))
|
||||
close()
|
||||
var event = new ConnectionEvent('error', {
|
||||
status: status,
|
||||
statusText: statusText,
|
||||
headers: headers,
|
||||
})
|
||||
es.dispatchEvent(event)
|
||||
fire(es, es.onerror, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var onProgress = function (textChunk) {
|
||||
if (currentState === OPEN) {
|
||||
var n = -1
|
||||
for (var i = 0; i < textChunk.length; i += 1) {
|
||||
var c = textChunk.charCodeAt(i)
|
||||
if (c === '\n'.charCodeAt(0) || c === '\r'.charCodeAt(0)) {
|
||||
n = i
|
||||
}
|
||||
}
|
||||
var chunk = (n !== -1 ? textBuffer : '') + textChunk.slice(0, n + 1)
|
||||
textBuffer = (n === -1 ? textBuffer : '') + textChunk.slice(n + 1)
|
||||
if (chunk !== '') {
|
||||
wasActivity = true
|
||||
}
|
||||
for (var position = 0; position < chunk.length; position += 1) {
|
||||
var c = chunk.charCodeAt(position)
|
||||
if (state === AFTER_CR && c === '\n'.charCodeAt(0)) {
|
||||
state = FIELD_START
|
||||
} else {
|
||||
if (state === AFTER_CR) {
|
||||
state = FIELD_START
|
||||
}
|
||||
if (c === '\r'.charCodeAt(0) || c === '\n'.charCodeAt(0)) {
|
||||
if (state !== FIELD_START) {
|
||||
if (state === FIELD) {
|
||||
valueStart = position + 1
|
||||
}
|
||||
var field = chunk.slice(fieldStart, valueStart - 1)
|
||||
var value = chunk.slice(
|
||||
valueStart +
|
||||
(valueStart < position &&
|
||||
chunk.charCodeAt(valueStart) === ' '.charCodeAt(0)
|
||||
? 1
|
||||
: 0),
|
||||
position
|
||||
)
|
||||
if (field === 'data') {
|
||||
dataBuffer += '\n'
|
||||
dataBuffer += value
|
||||
} else if (field === 'id') {
|
||||
lastEventIdBuffer = value
|
||||
} else if (field === 'event') {
|
||||
eventTypeBuffer = value
|
||||
} else if (field === 'retry') {
|
||||
initialRetry = parseDuration(value, initialRetry)
|
||||
retry = initialRetry
|
||||
} else if (field === 'heartbeatTimeout') {
|
||||
heartbeatTimeout = parseDuration(value, heartbeatTimeout)
|
||||
if (timeout !== 0) {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(function () {
|
||||
onTimeout()
|
||||
}, heartbeatTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state === FIELD_START) {
|
||||
if (dataBuffer !== '') {
|
||||
lastEventId = lastEventIdBuffer
|
||||
if (eventTypeBuffer === '') {
|
||||
eventTypeBuffer = 'message'
|
||||
}
|
||||
var event = new MessageEvent(eventTypeBuffer, {
|
||||
data: dataBuffer.slice(1),
|
||||
lastEventId: lastEventIdBuffer,
|
||||
})
|
||||
es.dispatchEvent(event)
|
||||
if (eventTypeBuffer === 'message') {
|
||||
fire(es, es.onmessage, event)
|
||||
}
|
||||
if (currentState === CLOSED) {
|
||||
return
|
||||
}
|
||||
}
|
||||
dataBuffer = ''
|
||||
eventTypeBuffer = ''
|
||||
}
|
||||
state = c === '\r'.charCodeAt(0) ? AFTER_CR : FIELD_START
|
||||
} else {
|
||||
if (state === FIELD_START) {
|
||||
fieldStart = position
|
||||
state = FIELD
|
||||
}
|
||||
if (state === FIELD) {
|
||||
if (c === ':'.charCodeAt(0)) {
|
||||
valueStart = position + 1
|
||||
state = VALUE_START
|
||||
}
|
||||
} else if (state === VALUE_START) {
|
||||
state = VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var onFinish = function () {
|
||||
if (currentState === OPEN || currentState === CONNECTING) {
|
||||
currentState = WAITING
|
||||
if (timeout !== 0) {
|
||||
clearTimeout(timeout)
|
||||
timeout = 0
|
||||
}
|
||||
timeout = setTimeout(function () {
|
||||
onTimeout()
|
||||
}, retry)
|
||||
retry = clampDuration(Math.min(initialRetry * 16, retry * 2))
|
||||
|
||||
es.readyState = CONNECTING
|
||||
var event = new Event('error')
|
||||
es.dispatchEvent(event)
|
||||
fire(es, es.onerror, event)
|
||||
}
|
||||
}
|
||||
|
||||
var close = function () {
|
||||
currentState = CLOSED
|
||||
if (cancelFunction != undefined) {
|
||||
cancelFunction()
|
||||
cancelFunction = undefined
|
||||
}
|
||||
if (timeout !== 0) {
|
||||
clearTimeout(timeout)
|
||||
timeout = 0
|
||||
}
|
||||
es.readyState = CLOSED
|
||||
}
|
||||
|
||||
var onTimeout = function () {
|
||||
timeout = 0
|
||||
|
||||
if (currentState !== WAITING) {
|
||||
if (!wasActivity && cancelFunction != undefined) {
|
||||
throwError(
|
||||
new Error(
|
||||
'No activity within ' +
|
||||
heartbeatTimeout +
|
||||
' milliseconds. Reconnecting.'
|
||||
)
|
||||
)
|
||||
cancelFunction()
|
||||
cancelFunction = undefined
|
||||
} else {
|
||||
wasActivity = false
|
||||
timeout = setTimeout(function () {
|
||||
onTimeout()
|
||||
}, heartbeatTimeout)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
wasActivity = false
|
||||
timeout = setTimeout(function () {
|
||||
onTimeout()
|
||||
}, heartbeatTimeout)
|
||||
|
||||
currentState = CONNECTING
|
||||
dataBuffer = ''
|
||||
eventTypeBuffer = ''
|
||||
lastEventIdBuffer = lastEventId
|
||||
textBuffer = ''
|
||||
fieldStart = 0
|
||||
valueStart = 0
|
||||
state = FIELD_START
|
||||
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=428916
|
||||
// Request header field Last-Event-ID is not allowed by Access-Control-Allow-Headers.
|
||||
var requestURL = url
|
||||
if (url.slice(0, 5) !== 'data:' && url.slice(0, 5) !== 'blob:') {
|
||||
if (lastEventId !== '') {
|
||||
requestURL +=
|
||||
(url.indexOf('?') === -1 ? '?' : '&') +
|
||||
'lastEventId=' +
|
||||
encodeURIComponent(lastEventId)
|
||||
}
|
||||
}
|
||||
var requestHeaders = {}
|
||||
requestHeaders['Accept'] = 'text/event-stream'
|
||||
if (headers != undefined) {
|
||||
for (var name in headers) {
|
||||
if (Object.prototype.hasOwnProperty.call(headers, name)) {
|
||||
requestHeaders[name] = headers[name]
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
transport.open(
|
||||
xhr,
|
||||
onStart,
|
||||
onProgress,
|
||||
onFinish,
|
||||
requestURL,
|
||||
withCredentials,
|
||||
requestHeaders
|
||||
)
|
||||
} catch (error) {
|
||||
close()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
es.url = url
|
||||
es.readyState = CONNECTING
|
||||
es.withCredentials = withCredentials
|
||||
es._close = close
|
||||
|
||||
onTimeout()
|
||||
}
|
||||
|
||||
EventSourcePolyfill.prototype = Object.create(EventTarget.prototype)
|
||||
EventSourcePolyfill.prototype.CONNECTING = CONNECTING
|
||||
EventSourcePolyfill.prototype.OPEN = OPEN
|
||||
EventSourcePolyfill.prototype.CLOSED = CLOSED
|
||||
EventSourcePolyfill.prototype.close = function () {
|
||||
this._close()
|
||||
}
|
||||
|
||||
EventSourcePolyfill.CONNECTING = CONNECTING
|
||||
EventSourcePolyfill.OPEN = OPEN
|
||||
EventSourcePolyfill.CLOSED = CLOSED
|
||||
EventSourcePolyfill.prototype.withCredentials = undefined
|
||||
|
||||
export default EventSourcePolyfill
|
|
@ -1,33 +1,46 @@
|
|||
import Router from 'next/router'
|
||||
import { setupPing, currentPage, closePing } from './on-demand-entries-utils'
|
||||
import { addMessageListener, sendMessage } from './error-overlay/websocket'
|
||||
|
||||
export default async ({ assetPrefix }) => {
|
||||
Router.ready(() => {
|
||||
Router.events.on(
|
||||
'routeChangeComplete',
|
||||
setupPing.bind(this, assetPrefix, () => Router.pathname)
|
||||
)
|
||||
})
|
||||
export default async () => {
|
||||
setInterval(() => {
|
||||
sendMessage(JSON.stringify({ event: 'ping', page: Router.pathname }))
|
||||
}, 2500)
|
||||
|
||||
setupPing(
|
||||
assetPrefix,
|
||||
() => Router.query.__NEXT_PAGE || Router.pathname,
|
||||
currentPage
|
||||
)
|
||||
|
||||
// prevent HMR connection from being closed when running tests
|
||||
if (!process.env.__NEXT_TEST_MODE) {
|
||||
document.addEventListener('visibilitychange', (_event) => {
|
||||
const state = document.visibilityState
|
||||
if (state === 'visible') {
|
||||
setupPing(assetPrefix, () => Router.pathname, true)
|
||||
} else {
|
||||
closePing()
|
||||
addMessageListener((event) => {
|
||||
if (event.data.indexOf('{') === -1) return
|
||||
try {
|
||||
const payload = JSON.parse(event.data)
|
||||
// don't attempt fetching the page if we're already showing
|
||||
// the dev overlay as this can cause the error to be triggered
|
||||
// repeatedly
|
||||
if (
|
||||
payload.event === 'pong' &&
|
||||
payload.invalid &&
|
||||
!self.__NEXT_DATA__.err
|
||||
) {
|
||||
// Payload can be invalid even if the page does exist.
|
||||
// So, we check if it can be created.
|
||||
fetch(location.href, {
|
||||
credentials: 'same-origin',
|
||||
}).then((pageRes) => {
|
||||
if (pageRes.status === 200) {
|
||||
// Page exists now, reload
|
||||
location.reload()
|
||||
} else {
|
||||
// Page doesn't exist
|
||||
if (
|
||||
self.__NEXT_DATA__.page === Router.pathname &&
|
||||
Router.pathname !== '/_error'
|
||||
) {
|
||||
// We are still on the page,
|
||||
// reload to show 404 error page
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
closePing()
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('on-demand-entries failed to parse response', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
/* global location */
|
||||
import { getEventSourceWrapper } from './error-overlay/eventsource'
|
||||
|
||||
let evtSource
|
||||
export let currentPage
|
||||
|
||||
export function closePing() {
|
||||
if (evtSource) evtSource.close()
|
||||
evtSource = null
|
||||
}
|
||||
|
||||
export function setupPing(assetPrefix, pathnameFn, retry) {
|
||||
const pathname = pathnameFn()
|
||||
|
||||
// Make sure to only create new EventSource request if page has changed
|
||||
if (pathname === currentPage && !retry) return
|
||||
currentPage = pathname
|
||||
// close current EventSource connection
|
||||
closePing()
|
||||
|
||||
evtSource = getEventSourceWrapper({
|
||||
path: `${assetPrefix}/_next/webpack-hmr?page=${encodeURIComponent(
|
||||
currentPage
|
||||
)}`,
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
evtSource.addMessageListener((event) => {
|
||||
if (event.data.indexOf('{') === -1) return
|
||||
try {
|
||||
const payload = JSON.parse(event.data)
|
||||
// don't attempt fetching the page if we're already showing
|
||||
// the dev overlay as this can cause the error to be triggered
|
||||
// repeatedly
|
||||
if (payload.invalid && !self.__NEXT_DATA__.err) {
|
||||
// Payload can be invalid even if the page does exist.
|
||||
// So, we check if it can be created.
|
||||
fetch(location.href, {
|
||||
credentials: 'same-origin',
|
||||
}).then((pageRes) => {
|
||||
if (pageRes.status === 200) {
|
||||
// Page exists now, reload
|
||||
location.reload()
|
||||
} else {
|
||||
// Page doesn't exist
|
||||
if (
|
||||
self.__NEXT_DATA__.page === currentPage &&
|
||||
currentPage !== '/_error'
|
||||
) {
|
||||
// We are still on the page,
|
||||
// reload to show 404 error page
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('on-demand-entries failed to parse response', err)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
/* globals __REPLACE_NOOP_IMPORT__ */
|
||||
import { initNext, version, router, emitter, render, renderError } from './'
|
||||
import EventSourcePolyfill from './dev/event-source-polyfill'
|
||||
import initOnDemandEntries from './dev/on-demand-entries-client'
|
||||
import initWebpackHMR from './dev/webpack-hot-middleware-client'
|
||||
import initializeBuildWatcher from './dev/dev-build-watcher'
|
||||
import { displayContent } from './dev/fouc'
|
||||
import { addMessageListener } from './dev/error-overlay/eventsource'
|
||||
import { connectHMR, addMessageListener } from './dev/error-overlay/websocket'
|
||||
import {
|
||||
assign,
|
||||
urlQueryToSearchParams,
|
||||
|
@ -18,11 +17,6 @@ import {
|
|||
// eslint-disable-next-line no-unused-expressions
|
||||
__REPLACE_NOOP_IMPORT__
|
||||
|
||||
// Support EventSource on Internet Explorer 11
|
||||
if (!window.EventSource) {
|
||||
window.EventSource = EventSourcePolyfill
|
||||
}
|
||||
|
||||
const {
|
||||
__NEXT_DATA__: { assetPrefix },
|
||||
} = window
|
||||
|
@ -30,6 +24,8 @@ const {
|
|||
const prefix = assetPrefix || ''
|
||||
const webpackHMR = initWebpackHMR()
|
||||
|
||||
connectHMR({ path: `${prefix}/_next/webpack-hmr` })
|
||||
|
||||
window.next = {
|
||||
version,
|
||||
// router is initialized later so it has to be live-binded
|
||||
|
@ -42,7 +38,7 @@ window.next = {
|
|||
}
|
||||
initNext({ webpackHMR })
|
||||
.then(({ renderCtx }) => {
|
||||
initOnDemandEntries({ assetPrefix: prefix })
|
||||
initOnDemandEntries()
|
||||
|
||||
let buildIndicatorHandler = () => {}
|
||||
|
||||
|
@ -88,7 +84,6 @@ initNext({ webpackHMR })
|
|||
}
|
||||
}
|
||||
}
|
||||
devPagesManifestListener.unfiltered = true
|
||||
addMessageListener(devPagesManifestListener)
|
||||
|
||||
if (process.env.__NEXT_BUILD_INDICATOR) {
|
||||
|
|
19
packages/next/compiled/ws/LICENSE
Normal file
19
packages/next/compiled/ws/LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
1
packages/next/compiled/ws/index.js
Normal file
1
packages/next/compiled/ws/index.js
Normal file
File diff suppressed because one or more lines are too long
1
packages/next/compiled/ws/package.json
Normal file
1
packages/next/compiled/ws/package.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"name":"ws","main":"index.js","author":"Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)","license":"MIT"}
|
|
@ -183,6 +183,7 @@
|
|||
"@types/styled-jsx": "2.2.8",
|
||||
"@types/text-table": "0.2.1",
|
||||
"@types/webpack-sources1": "npm:@types/webpack-sources@0.1.5",
|
||||
"@types/ws": "8.2.0",
|
||||
"@vercel/ncc": "0.27.0",
|
||||
"@vercel/nft": "0.16.1",
|
||||
"amphtml-validator": "1.0.33",
|
||||
|
@ -249,7 +250,8 @@
|
|||
"webpack-sources1": "npm:webpack-sources@1.4.3",
|
||||
"webpack-sources3": "npm:webpack-sources@3.2.0",
|
||||
"webpack4": "npm:webpack@4.44.1",
|
||||
"webpack5": "npm:webpack@5.58.2"
|
||||
"webpack5": "npm:webpack@5.58.2",
|
||||
"ws": "8.2.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"browserslist": "4.16.6",
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
import { webpack } from 'next/dist/compiled/webpack/webpack'
|
||||
import http from 'http'
|
||||
import type ws from 'ws'
|
||||
|
||||
export class WebpackHotMiddleware {
|
||||
eventStream: EventStream
|
||||
|
@ -86,14 +86,10 @@ export class WebpackHotMiddleware {
|
|||
this.latestStats = statsResult
|
||||
this.publishStats('built', this.latestStats)
|
||||
}
|
||||
middleware = (
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
next: () => void
|
||||
) => {
|
||||
if (this.closed) return next()
|
||||
if (!req.url?.startsWith('/_next/webpack-hmr')) return next()
|
||||
this.eventStream.handler(req, res)
|
||||
|
||||
onHMR = (client: ws) => {
|
||||
if (this.closed) return
|
||||
this.eventStream.handler(client)
|
||||
if (this.latestStats) {
|
||||
// Explicitly not passing in `log` fn as we don't want to log again on
|
||||
// the server
|
||||
|
@ -131,64 +127,34 @@ export class WebpackHotMiddleware {
|
|||
}
|
||||
|
||||
class EventStream {
|
||||
clients: Set<http.ServerResponse>
|
||||
interval: NodeJS.Timeout
|
||||
clients: Set<ws>
|
||||
constructor() {
|
||||
this.clients = new Set()
|
||||
|
||||
this.interval = setInterval(this.heartbeatTick, 2500).unref()
|
||||
}
|
||||
|
||||
heartbeatTick = () => {
|
||||
this.everyClient((client) => {
|
||||
client.write('data: \uD83D\uDC93\n\n')
|
||||
})
|
||||
}
|
||||
|
||||
everyClient(fn: (client: http.ServerResponse) => void) {
|
||||
everyClient(fn: (client: ws) => void) {
|
||||
for (const client of this.clients) {
|
||||
fn(client)
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
clearInterval(this.interval)
|
||||
this.everyClient((client) => {
|
||||
if (!client.finished) client.end()
|
||||
client.close()
|
||||
})
|
||||
this.clients.clear()
|
||||
}
|
||||
|
||||
handler(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
const headers = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Type': 'text/event-stream;charset=utf-8',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
// While behind nginx, event stream should not be buffered:
|
||||
// http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering
|
||||
'X-Accel-Buffering': 'no',
|
||||
}
|
||||
|
||||
const isHttp1 = !(parseInt(req.httpVersion) >= 2)
|
||||
if (isHttp1) {
|
||||
req.socket.setKeepAlive(true)
|
||||
Object.assign(headers, {
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
}
|
||||
|
||||
res.writeHead(200, headers)
|
||||
res.write('\n')
|
||||
this.clients.add(res)
|
||||
req.on('close', () => {
|
||||
if (!res.finished) res.end()
|
||||
this.clients.delete(res)
|
||||
handler(client: ws) {
|
||||
this.clients.add(client)
|
||||
client.addEventListener('close', () => {
|
||||
this.clients.delete(client)
|
||||
})
|
||||
}
|
||||
|
||||
publish(payload: any) {
|
||||
this.everyClient((client) => {
|
||||
client.write('data: ' + JSON.stringify(payload) + '\n\n')
|
||||
client.send(JSON.stringify(payload))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { getOverlayMiddleware } from '@next/react-dev-overlay/lib/middleware'
|
||||
import { NextHandleFunction } from 'connect'
|
||||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
import { WebpackHotMiddleware } from './hot-middleware'
|
||||
import { join, relative, isAbsolute } from 'path'
|
||||
|
@ -33,6 +32,9 @@ import { CustomRoutes } from '../../lib/load-custom-routes'
|
|||
import { DecodeError } from '../../shared/lib/utils'
|
||||
import { Span, trace } from '../../trace'
|
||||
import isError from '../../lib/is-error'
|
||||
import ws from 'next/dist/compiled/ws'
|
||||
|
||||
const wsServer = new ws.Server({ noServer: true })
|
||||
|
||||
export async function renderScriptError(
|
||||
res: ServerResponse,
|
||||
|
@ -136,7 +138,7 @@ export default class HotReloader {
|
|||
private buildId: string
|
||||
private middlewares: any[]
|
||||
private pagesDir: string
|
||||
private webpackHotMiddleware: (NextHandleFunction & any) | null
|
||||
private webpackHotMiddleware?: WebpackHotMiddleware
|
||||
private config: NextConfigComplete
|
||||
private stats: webpack.Stats | null
|
||||
public serverStats: webpack.Stats | null
|
||||
|
@ -144,7 +146,7 @@ export default class HotReloader {
|
|||
private serverError: Error | null = null
|
||||
private serverPrevDocumentHash: string | null
|
||||
private prevChunkNames?: Set<any>
|
||||
private onDemandEntries: any
|
||||
private onDemandEntries?: ReturnType<typeof onDemandEntryHandler>
|
||||
private previewProps: __ApiPreviewProps
|
||||
private watcher: any
|
||||
private rewrites: CustomRoutes['rewrites']
|
||||
|
@ -171,7 +173,6 @@ export default class HotReloader {
|
|||
this.dir = dir
|
||||
this.middlewares = []
|
||||
this.pagesDir = pagesDir
|
||||
this.webpackHotMiddleware = null
|
||||
this.stats = null
|
||||
this.serverStats = null
|
||||
this.serverPrevDocumentHash = null
|
||||
|
@ -261,6 +262,13 @@ export default class HotReloader {
|
|||
return { finished }
|
||||
}
|
||||
|
||||
public onHMR(req: IncomingMessage, _res: ServerResponse, head: Buffer) {
|
||||
wsServer.handleUpgrade(req, req.socket, head, (client) => {
|
||||
this.webpackHotMiddleware?.onHMR(client)
|
||||
this.onDemandEntries?.onHMR(client)
|
||||
})
|
||||
}
|
||||
|
||||
private async clean(span: Span): Promise<void> {
|
||||
return span
|
||||
.traceChild('clean')
|
||||
|
@ -623,9 +631,6 @@ export default class HotReloader {
|
|||
})
|
||||
|
||||
this.middlewares = [
|
||||
// must come before hotMiddleware
|
||||
this.onDemandEntries.middleware,
|
||||
this.webpackHotMiddleware.middleware,
|
||||
getOverlayMiddleware({
|
||||
rootDirectory: this.dir,
|
||||
stats: () => this.stats,
|
||||
|
@ -703,7 +708,7 @@ export default class HotReloader {
|
|||
if (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
return this.onDemandEntries.ensurePage(page, clientOnly)
|
||||
return this.onDemandEntries?.ensurePage(page, clientOnly) as any
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import crypto from 'crypto'
|
||||
import fs from 'fs'
|
||||
import chalk from 'chalk'
|
||||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
import { IncomingMessage, ServerResponse, Server as HTTPServer } from 'http'
|
||||
import { Worker } from 'jest-worker'
|
||||
import AmpHtmlValidator from 'next/dist/compiled/amphtml-validator'
|
||||
import findUp from 'next/dist/compiled/find-up'
|
||||
|
@ -73,6 +73,7 @@ export default class DevServer extends Server {
|
|||
private hotReloader?: HotReloader
|
||||
private isCustomServer: boolean
|
||||
protected sortedRoutes?: string[]
|
||||
private addedUpgradeListener = false
|
||||
|
||||
protected staticPathsWorker: import('jest-worker').Worker & {
|
||||
loadStaticPaths: typeof import('./static-paths-worker').loadStaticPaths
|
||||
|
@ -82,6 +83,7 @@ export default class DevServer extends Server {
|
|||
options: ServerConstructor & {
|
||||
conf: NextConfig
|
||||
isNextDevCommand?: boolean
|
||||
httpServer?: HTTPServer
|
||||
}
|
||||
) {
|
||||
super({ ...options, dev: true })
|
||||
|
@ -116,6 +118,13 @@ export default class DevServer extends Server {
|
|||
`The static directory has been deprecated in favor of the public directory. https://nextjs.org/docs/messages/static-dir-deprecated`
|
||||
)
|
||||
}
|
||||
|
||||
// setup upgrade listener eagerly when we can otherwise
|
||||
// it will be done on the first request via req.socket.server
|
||||
if (options.httpServer) {
|
||||
this.setupWebSocketHandler(options.httpServer)
|
||||
}
|
||||
|
||||
this.isCustomServer = !options.isNextDevCommand
|
||||
this.pagesDir = findPagesDir(this.dir)
|
||||
this.staticPathsWorker = new Worker(
|
||||
|
@ -410,12 +419,38 @@ export default class DevServer extends Server {
|
|||
return false
|
||||
}
|
||||
|
||||
private setupWebSocketHandler(server?: HTTPServer, _req?: IncomingMessage) {
|
||||
if (!this.addedUpgradeListener) {
|
||||
this.addedUpgradeListener = true
|
||||
server = server || (_req?.socket as any)?.server
|
||||
|
||||
if (!server) {
|
||||
// this is very unlikely to happen but show an error in case
|
||||
// it does somehow
|
||||
Log.error(
|
||||
`Invalid IncomingMessage received, make sure http.createServer is being used to handle requests.`
|
||||
)
|
||||
} else {
|
||||
server.on('upgrade', (req, socket, head) => {
|
||||
if (
|
||||
req.url?.startsWith(
|
||||
`${this.nextConfig.basePath || ''}/_next/webpack-hmr`
|
||||
)
|
||||
) {
|
||||
this.hotReloader?.onHMR(req, socket, head)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async run(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
parsedUrl: UrlWithParsedQuery
|
||||
): Promise<void> {
|
||||
await this.devReady
|
||||
this.setupWebSocketHandler(undefined, req)
|
||||
|
||||
const { basePath } = this.nextConfig
|
||||
let originalPathname: string | null = null
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
import { join, posix } from 'path'
|
||||
import { parse } from 'url'
|
||||
import { webpack } from 'next/dist/compiled/webpack/webpack'
|
||||
import { normalizePagePath, normalizePathSep } from '../normalize-page-path'
|
||||
import { pageNotFoundError } from '../require'
|
||||
|
@ -9,6 +7,7 @@ import { findPageFile } from '../lib/find-page-file'
|
|||
import getRouteFromEntrypoint from '../get-route-from-entrypoint'
|
||||
import { API_ROUTE } from '../../lib/constants'
|
||||
import { reportTrigger } from '../../build/output'
|
||||
import type ws from 'ws'
|
||||
|
||||
export const ADDED = Symbol('added')
|
||||
export const BUILDING = Symbol('building')
|
||||
|
@ -247,27 +246,23 @@ export default function onDemandEntryHandler(
|
|||
return promise
|
||||
},
|
||||
|
||||
middleware(req: IncomingMessage, res: ServerResponse, next: Function) {
|
||||
if (!req.url?.startsWith('/_next/webpack-hmr')) return next()
|
||||
onHMR(client: ws) {
|
||||
client.addEventListener('message', ({ data }) => {
|
||||
data = typeof data !== 'string' ? data.toString() : data
|
||||
try {
|
||||
const parsedData = JSON.parse(data)
|
||||
|
||||
const { query } = parse(req.url!, true)
|
||||
const page = query.page
|
||||
if (!page) return next()
|
||||
|
||||
const runPing = () => {
|
||||
const data = handlePing(query.page as string)
|
||||
if (!data) return
|
||||
res.write('data: ' + JSON.stringify(data) + '\n\n')
|
||||
}
|
||||
const pingInterval = setInterval(() => runPing(), pingIntervalTime)
|
||||
|
||||
// Run a ping now to make sure page is instantly flagged as active
|
||||
setTimeout(() => runPing(), 0)
|
||||
|
||||
req.on('close', () => {
|
||||
clearInterval(pingInterval)
|
||||
if (parsedData.event === 'ping') {
|
||||
const result = handlePing(parsedData.page)
|
||||
client.send(
|
||||
JSON.stringify({
|
||||
...result,
|
||||
event: 'pong',
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (_) {}
|
||||
})
|
||||
next()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,17 @@ export default async function start(
|
|||
port?: number,
|
||||
hostname?: string
|
||||
) {
|
||||
let requestHandler: ReturnType<typeof app.getRequestHandler>
|
||||
const srv = http.createServer((req, res) => {
|
||||
return requestHandler(req, res)
|
||||
})
|
||||
const app = next({
|
||||
...serverOptions,
|
||||
customServer: false,
|
||||
httpServer: srv,
|
||||
})
|
||||
const srv = http.createServer(app.getRequestHandler())
|
||||
requestHandler = app.getRequestHandler()
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// This code catches EADDRINUSE error if the port is already in use
|
||||
srv.on('error', reject)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const findUp = require('find-up')
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const ncc = require('@vercel/ncc')
|
||||
const { existsSync, readFileSync } = require('fs')
|
||||
const { basename, dirname, extname, join, resolve } = require('path')
|
||||
|
@ -56,7 +58,17 @@ module.exports = function (task) {
|
|||
// It defines `name`, `main`, `author`, and `license`. It also defines `types`.
|
||||
// n.b. types intended for development usage only.
|
||||
function writePackageManifest(packageName, main, bundleName, precompiled) {
|
||||
const packagePath = bundleRequire.resolve(packageName + '/package.json')
|
||||
// some newer packages fail to include package.json in the exports
|
||||
// so we can't reliably use require.resolve here
|
||||
let packagePath
|
||||
|
||||
try {
|
||||
packagePath = bundleRequire.resolve(packageName + '/package.json')
|
||||
} catch (_) {
|
||||
packagePath = findUp.sync('package.json', {
|
||||
cwd: dirname(bundleRequire.resolve(packageName)),
|
||||
})
|
||||
}
|
||||
let { name, author, license } = require(packagePath)
|
||||
|
||||
const compiledPackagePath = join(
|
||||
|
|
|
@ -782,6 +782,15 @@ export async function ncc_webpack_bundle_packages(task, opts) {
|
|||
.target('compiled/webpack/')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
externals['ws'] = 'next/dist/compiled/ws'
|
||||
export async function ncc_ws(task, opts) {
|
||||
await task
|
||||
.source(opts.src || relative(__dirname, require.resolve('ws')))
|
||||
.ncc({ packageName: 'ws', externals })
|
||||
.target('compiled/ws')
|
||||
}
|
||||
|
||||
externals['path-to-regexp'] = 'next/dist/compiled/path-to-regexp'
|
||||
export async function path_to_regexp(task, opts) {
|
||||
await task
|
||||
|
@ -871,6 +880,7 @@ export async function ncc(task, opts) {
|
|||
'ncc_webpack_bundle_packages',
|
||||
'ncc_webpack_sources1',
|
||||
'ncc_webpack_sources3',
|
||||
'ncc_ws',
|
||||
'ncc_mini_css_extract_plugin',
|
||||
],
|
||||
opts
|
||||
|
|
4
packages/next/types/misc.d.ts
vendored
4
packages/next/types/misc.d.ts
vendored
|
@ -218,6 +218,10 @@ declare module 'next/dist/compiled/web-vitals' {
|
|||
import m from 'web-vitals'
|
||||
export = m
|
||||
}
|
||||
declare module 'next/dist/compiled/ws' {
|
||||
import m from 'ws'
|
||||
export = m
|
||||
}
|
||||
|
||||
declare module 'next/dist/compiled/comment-json' {
|
||||
import m from 'comment-json'
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export default () => <div>API - conflict</div>
|
||||
export default function Page() {
|
||||
return <div>API - conflict</div>
|
||||
}
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export default () => <div>API - support</div>
|
||||
export default function Page() {
|
||||
return <div>API - support</div>
|
||||
}
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export default () => <div>User</div>
|
||||
export default function Page() {
|
||||
return <div>User</div>
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
nextExport,
|
||||
getPageFileFromBuildManifest,
|
||||
getPageFileFromPagesManifest,
|
||||
check,
|
||||
} from 'next-test-utils'
|
||||
import json from '../big.json'
|
||||
|
||||
|
@ -479,8 +480,10 @@ function runTests(dev = false) {
|
|||
await fetchViaHTTP(appPort, '/api/test-no-end', undefined, {
|
||||
signal: controller.signal,
|
||||
}).catch(() => {})
|
||||
expect(stderr).toContain(
|
||||
`API resolved without sending a response for /api/test-no-end, this may result in stalled requests.`
|
||||
|
||||
await check(
|
||||
() => stderr,
|
||||
/API resolved without sending a response for \/api\/test-no-end, this may result in stalled requests/
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
import webdriver from 'next-webdriver'
|
||||
import WebSocket from 'ws'
|
||||
import { join } from 'path'
|
||||
import AbortController from 'abort-controller'
|
||||
import webdriver from 'next-webdriver'
|
||||
import {
|
||||
renderViaHTTP,
|
||||
fetchViaHTTP,
|
||||
findPort,
|
||||
launchApp,
|
||||
killApp,
|
||||
|
@ -20,22 +18,9 @@ const appDir = join(__dirname, '../')
|
|||
const context = {}
|
||||
|
||||
const doPing = (page) => {
|
||||
const controller = new AbortController()
|
||||
const signal = controller.signal
|
||||
return fetchViaHTTP(
|
||||
context.appPort,
|
||||
'/_next/webpack-hmr',
|
||||
{ page },
|
||||
{ signal }
|
||||
).then((res) => {
|
||||
res.body.on('data', (chunk) => {
|
||||
try {
|
||||
const payload = JSON.parse(chunk.toString().split('data:')[1])
|
||||
if (payload.success || payload.invalid) {
|
||||
controller.abort()
|
||||
}
|
||||
} catch (_) {}
|
||||
})
|
||||
return new Promise((resolve) => {
|
||||
context.ws.onmessage = () => resolve()
|
||||
context.ws.send(JSON.stringify({ event: 'ping', page }))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -44,8 +29,17 @@ describe('On Demand Entries', () => {
|
|||
beforeAll(async () => {
|
||||
context.appPort = await findPort()
|
||||
context.server = await launchApp(appDir, context.appPort)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
context.ws = new WebSocket(
|
||||
`ws://localhost:${context.appPort}/_next/webpack-hmr`
|
||||
)
|
||||
context.ws.on('open', () => resolve())
|
||||
context.ws.on('error', console.error)
|
||||
})
|
||||
})
|
||||
afterAll(() => {
|
||||
context.ws.close()
|
||||
killApp(context.server)
|
||||
})
|
||||
|
||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -4867,6 +4867,13 @@
|
|||
"@types/source-list-map" "*"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@types/ws@8.2.0":
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.0.tgz#75faefbe2328f3b833cb8dc640658328990d04f3"
|
||||
integrity sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/yargs-parser@*":
|
||||
version "13.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228"
|
||||
|
@ -17277,6 +17284,7 @@ scheduler@^0.20.2:
|
|||
ajv-keywords "^3.5.2"
|
||||
|
||||
"schema-utils3@npm:schema-utils@3.0.0", schema-utils@^3.0.0:
|
||||
name schema-utils3
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef"
|
||||
integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==
|
||||
|
@ -19989,6 +19997,11 @@ write-pkg@^4.0.0:
|
|||
type-fest "^0.4.1"
|
||||
write-json-file "^3.2.0"
|
||||
|
||||
ws@8.2.3:
|
||||
version "8.2.3"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
|
||||
integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
|
||||
|
||||
ws@>=7.4.6:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.0.tgz#0b738cd484bfc9303421914b11bb4011e07615bb"
|
||||
|
|
Loading…
Reference in a new issue